Skip to main content
Back to Blog
Tutorials
3 min read
December 12, 2024

How to Build a Code Syntax Highlighter Component in React

Build a code syntax highlighter component using Shiki with line numbers, copy button, language badge, and line highlighting.

Ryel Banfield

Founder & Lead Developer

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.

syntax highlightingcodeShikiReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles