{"content":"# Shiki Code Blocks\n\nSyntax highlight code blocks with Shiki. Supports server-side rendering in RSC and automatic light/dark theme switching.\n\n# Shiki Code Blocks\n\nSyntax highlight code blocks with Shiki. Supports server-side rendering in React Server Components and automatic light/dark theme switching.\n\n## File Structure\n\n```\nsrc/components/code/\n  code-block.tsx        # Code block with copy button\n  copy-button.tsx       # Copy to clipboard button\n```\n\n---\n\n## Setup\n\n### Step 1: Install Shiki\n\n```bash\nbun add shiki\n```\n\n### Step 2: Create the copy button component\n\n```tsx\n// src/components/code/copy-button.tsx\n\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype CopyButtonProps = {\n  text: string;\n  timeout?: number;\n  className?: string;\n};\n\nexport function CopyButton({\n  text,\n  timeout = 2000,\n  className,\n}: CopyButtonProps) {\n  const [isCopied, setIsCopied] = useState(false);\n\n  const copyToClipboard = async () => {\n    if (typeof window === \"undefined\" || !navigator?.clipboard?.writeText) {\n      return;\n    }\n\n    try {\n      await navigator.clipboard.writeText(text);\n      setIsCopied(true);\n      setTimeout(() => setIsCopied(false), timeout);\n    } catch {\n      // Silently fail\n    }\n  };\n\n  const Icon = isCopied ? CheckIcon : CopyIcon;\n  const ariaLabel = isCopied ? \"Copied!\" : \"Copy to clipboard\";\n\n  return (\n    <Button\n      className={cn(\n        \"size-6 opacity-0 transition-opacity group-hover:opacity-100\",\n        className,\n      )}\n      onClick={copyToClipboard}\n      size=\"icon\"\n      variant=\"ghost\"\n      aria-label={ariaLabel}\n      title={ariaLabel}\n    >\n      <Icon size={12} />\n    </Button>\n  );\n}\n```\n\n### Step 3: Create the code block component\n\n```tsx\n// src/components/code/code-block.tsx\n\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { CopyButton } from \"@/components/code/copy-button\";\n\ntype CodeBlockProps = {\n  code: string;\n  language: string;\n  lightHtml: string;\n  darkHtml: string;\n  className?: string;\n};\n\nexport function CodeBlock({\n  code,\n  language,\n  lightHtml,\n  darkHtml,\n  className,\n}: CodeBlockProps) {\n  return (\n    <div\n      className={cn(\n        \"group relative w-full overflow-hidden rounded-md border bg-background\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center justify-between border-b bg-muted/40 px-3 py-2\">\n        <span className=\"rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground\">\n          {language}\n        </span>\n        <CopyButton text={code} />\n      </div>\n      <div className=\"relative\">\n        <div\n          className=\"overflow-x-auto dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm\"\n          dangerouslySetInnerHTML={{ __html: lightHtml }}\n        />\n        <div\n          className=\"hidden overflow-x-auto dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm\"\n          dangerouslySetInnerHTML={{ __html: darkHtml }}\n        />\n      </div>\n    </div>\n  );\n}\n```\n\n### Step 4: Create the highlight utility\n\n```tsx\n// src/lib/code/highlight.ts\nimport { codeToHtml, type BundledLanguage } from \"shiki\";\n\nexport async function highlightCode(code: string, language: BundledLanguage) {\n  const [light, dark] = await Promise.all([\n    codeToHtml(code, {\n      lang: language,\n      theme: \"one-light\",\n    }),\n    codeToHtml(code, {\n      lang: language,\n      theme: \"one-dark-pro\",\n    }),\n  ]);\n  return { light, dark };\n}\n```\n\n---\n\n## Usage\n\n### Server-side highlighting (React Server Components)\n\nHighlight code at build time or request time in a Server Component:\n\n```tsx\n// src/components/docs/code-example.tsx\nimport { CodeBlock } from \"@/components/code/code-block\";\nimport { highlightCode } from \"@/lib/code/highlight\";\n\ntype CodeExampleProps = {\n  code: string;\n  language: string;\n};\n\nexport async function CodeExample({ code, language }: CodeExampleProps) {\n  const { light, dark } = await highlightCode(code, language as any);\n\n  return (\n    <CodeBlock\n      code={code}\n      language={language}\n      lightHtml={light}\n      darkHtml={dark}\n    />\n  );\n}\n```\n\nUse in a page:\n\n```tsx\n// src/app/docs/page.tsx\nimport { CodeExample } from \"@/components/docs/code-example\";\n\nconst exampleCode = `const greeting = \"Hello, World!\";\nconsole.log(greeting);`;\n\nexport default function DocsPage() {\n  return (\n    <div className=\"space-y-4\">\n      <h1>Documentation</h1>\n      <CodeExample code={exampleCode} language=\"typescript\" />\n    </div>\n  );\n}\n```\n\n### Client-side highlighting\n\nFor dynamic code that changes at runtime:\n\n```tsx\n// src/components/code/dynamic-code-block.tsx\n\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { codeToHtml, type BundledLanguage } from \"shiki\";\nimport { CodeBlock } from \"@/components/code/code-block\";\n\ntype DynamicCodeBlockProps = {\n  code: string;\n  language: BundledLanguage;\n};\n\nexport function DynamicCodeBlock({ code, language }: DynamicCodeBlockProps) {\n  const [html, setHtml] = useState<{ light: string; dark: string } | null>(\n    null,\n  );\n\n  useEffect(() => {\n    let mounted = true;\n\n    Promise.all([\n      codeToHtml(code, { lang: language, theme: \"one-light\" }),\n      codeToHtml(code, { lang: language, theme: \"one-dark-pro\" }),\n    ]).then(([light, dark]) => {\n      if (mounted) {\n        setHtml({ light, dark });\n      }\n    });\n\n    return () => {\n      mounted = false;\n    };\n  }, [code, language]);\n\n  if (!html) {\n    return (\n      <div className=\"rounded-md border bg-background p-4\">\n        <pre className=\"overflow-x-auto font-mono text-sm\">\n          <code>{code}</code>\n        </pre>\n      </div>\n    );\n  }\n\n  return (\n    <CodeBlock\n      code={code}\n      language={language}\n      lightHtml={html.light}\n      darkHtml={html.dark}\n    />\n  );\n}\n```\n\n### Supported languages\n\nShiki supports all TextMate grammar languages. Common languages:\n\n```typescript\nconst SUPPORTED_LANGUAGES = [\n  \"typescript\",\n  \"javascript\",\n  \"tsx\",\n  \"jsx\",\n  \"json\",\n  \"bash\",\n  \"shell\",\n  \"css\",\n  \"html\",\n  \"sql\",\n  \"yaml\",\n  \"markdown\",\n  \"python\",\n  \"go\",\n  \"rust\",\n  \"dotenv\",\n] as const;\n```\n\n### Available themes\n\nUse different themes for light and dark modes:\n\n```typescript\n// Light themes\n\"one-light\";\n\"github-light\";\n\"vitesse-light\";\n\n// Dark themes\n\"one-dark-pro\";\n\"github-dark\";\n\"vitesse-dark\";\n\"dracula\";\n\"nord\";\n```\n\n### Adding line numbers\n\nUse a Shiki transformer to add line numbers:\n\n```typescript\nimport { codeToHtml, type ShikiTransformer } from \"shiki\";\n\nconst lineNumberTransformer: ShikiTransformer = {\n  name: \"line-numbers\",\n  line(node, line) {\n    node.children.unshift({\n      type: \"element\",\n      tagName: \"span\",\n      properties: {\n        className: [\n          \"inline-block\",\n          \"min-w-10\",\n          \"mr-4\",\n          \"text-right\",\n          \"select-none\",\n          \"text-muted-foreground\",\n        ],\n      },\n      children: [{ type: \"text\", value: String(line) }],\n    });\n  },\n};\n\nconst html = await codeToHtml(code, {\n  lang: \"typescript\",\n  theme: \"one-dark-pro\",\n  transformers: [lineNumberTransformer],\n});\n```\n\n---\n\n## References\n\n- [Shiki Documentation](https://shiki.style/)\n- [Shiki Languages](https://shiki.style/languages)\n- [Shiki Themes](https://shiki.style/themes)"}