Syntax highlighting improves readability in documentation, blogs, and developer tools. Shiki uses the same grammar engine as VS Code for accurate highlighting.
Install Shiki
pnpm add shiki
Server Component Highlighter
Shiki runs on the server, avoiding shipping grammar bundles to the client.
// components/CodeBlock.tsx
import { codeToHtml } from "shiki";
import { CopyButton } from "./CopyButton";
interface CodeBlockProps {
code: string;
language?: string;
filename?: string;
highlightLines?: number[];
showLineNumbers?: boolean;
}
export async function CodeBlock({
code,
language = "typescript",
filename,
highlightLines = [],
showLineNumbers = true,
}: CodeBlockProps) {
const html = await codeToHtml(code.trim(), {
lang: language,
themes: {
light: "github-light",
dark: "github-dark",
},
transformers: [
{
line(node, line) {
// Add line numbers
if (showLineNumbers) {
node.properties["data-line"] = line;
}
// Highlight specific lines
if (highlightLines.includes(line)) {
this.addClassToHast(node, "highlighted");
}
},
},
],
});
return (
<div className="group relative rounded-lg border overflow-hidden my-4">
{/* Header */}
{(filename || language) && (
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50">
<div className="flex items-center gap-2">
{filename && (
<span className="text-xs font-mono text-muted-foreground">
{filename}
</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground uppercase">
{language}
</span>
<CopyButton text={code.trim()} />
</div>
</div>
)}
{/* Code */}
<div
className="overflow-x-auto text-sm [&_pre]:!bg-transparent [&_pre]:p-4 [&_pre]:m-0 [&_.line]:px-4 [&_.line]:border-l-2 [&_.line]:border-transparent [&_.highlighted]:border-l-primary [&_.highlighted]:bg-primary/5"
dangerouslySetInnerHTML={{ __html: html }}
/>
{/* Line numbers styling */}
{showLineNumbers && (
<style>{`
[data-line]::before {
content: attr(data-line);
display: inline-block;
width: 2rem;
margin-right: 1rem;
text-align: right;
color: var(--color-muted-foreground);
opacity: 0.5;
font-size: 0.75rem;
user-select: none;
}
`}</style>
)}
</div>
);
}
Copy Button
"use client";
import { useState } from "react";
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
async function handleCopy() {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<button
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 transition-opacity px-2 py-1 rounded text-xs hover:bg-muted"
aria-label="Copy code"
>
{copied ? (
<span className="text-green-500">Copied</span>
) : (
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
)}
</button>
);
}
Multi-Tab Code Block
Show the same example in different languages or files.
"use client";
import { useState } from "react";
interface Tab {
label: string;
content: React.ReactNode;
}
export function CodeTabs({ tabs }: { tabs: Tab[] }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div className="rounded-lg border overflow-hidden my-4">
<div className="flex border-b bg-muted/50">
{tabs.map((tab, i) => (
<button
key={tab.label}
onClick={() => setActiveTab(i)}
className={`px-4 py-2 text-xs font-mono transition-colors ${
i === activeTab
? "bg-background border-b-2 border-primary font-medium"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab.label}
</button>
))}
</div>
<div>{tabs[activeTab]?.content}</div>
</div>
);
}
Inline Code Highlight
For inline code snippets within text.
// components/InlineCode.tsx
import { codeToHtml } from "shiki";
interface InlineCodeProps {
code: string;
language?: string;
}
export async function InlineCode({ code, language = "typescript" }: InlineCodeProps) {
const html = await codeToHtml(code, {
lang: language,
themes: {
light: "github-light",
dark: "github-dark",
},
});
return (
<span
className="inline rounded px-1.5 py-0.5 text-sm bg-muted [&_pre]:inline [&_pre]:!bg-transparent [&_code]:!bg-transparent"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
Diff View
Show code changes with added/removed lines.
// components/DiffBlock.tsx
import { codeToHtml } from "shiki";
interface DiffBlockProps {
code: string;
language?: string;
}
export async function DiffBlock({ code, language = "typescript" }: DiffBlockProps) {
const html = await codeToHtml(code.trim(), {
lang: language,
themes: {
light: "github-light",
dark: "github-dark",
},
transformers: [
{
line(node) {
const text = this.source.split("\n")[this.lineIndex] ?? "";
if (text.startsWith("+")) {
this.addClassToHast(node, "diff-add");
} else if (text.startsWith("-")) {
this.addClassToHast(node, "diff-remove");
}
},
},
],
});
return (
<div className="rounded-lg border overflow-hidden my-4">
<div
className="overflow-x-auto text-sm [&_pre]:!bg-transparent [&_pre]:p-4 [&_pre]:m-0 [&_.diff-add]:bg-green-500/10 [&_.diff-add]:border-l-2 [&_.diff-add]:border-green-500 [&_.diff-remove]:bg-red-500/10 [&_.diff-remove]:border-l-2 [&_.diff-remove]:border-red-500"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}
Usage in MDX or Pages
import { CodeBlock } from "@/components/CodeBlock";
import { CodeTabs } from "@/components/CodeTabs";
export default async function DocsPage() {
const exampleCode = `
export function greet(name: string): string {
return \`Hello, \${name}!\`;
}
const message = greet("World");
console.log(message);
`.trim();
return (
<article className="prose max-w-3xl mx-auto py-12">
<h1>Getting Started</h1>
<p>Create a simple greeting function:</p>
<CodeBlock
code={exampleCode}
language="typescript"
filename="lib/greet.ts"
highlightLines={[2]}
/>
<CodeTabs
tabs={[
{
label: "TypeScript",
content: <CodeBlock code={exampleCode} language="typescript" />,
},
{
label: "JavaScript",
content: (
<CodeBlock
code={`function greet(name) {\n return \`Hello, \${name}!\`;\n}`}
language="javascript"
/>
),
},
]}
/>
</article>
);
}
Need Developer Documentation?
We build documentation sites with beautiful code highlighting, API references, and interactive examples. Contact us to create your developer docs.