Code Block

Syntax-highlighted code display with copy support.


Installation

CLI

npx shadcn@latest add "https://ui.xaclabs.dev/r/code-block.json"

Usage

import { CodeBlock } from "@/registry/components/code-block"
View component source
code-block.tsx
tsx
"use client"

import { Check, Copy, FileCode } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import * as React from "react"
import { codeToHtml } from "shiki"
import { cn } from "@/lib/utils"

// ── Shared copy button with animated feedback ───────────────────

export function CopyButton({
  getText,
  className,
}: {
  getText: () => string
  className?: string
}) {
  const [copied, setCopied] = React.useState(false)

  const handleCopy = React.useCallback(() => {
    navigator.clipboard.writeText(getText()).then(() => {
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    })
  }, [getText])

  return (
    <button
      type="button"
      onClick={handleCopy}
      className={cn(
        "absolute right-3 top-3 z-10 flex items-center gap-1 rounded-md border bg-background/80 px-2 py-1 text-xs text-muted-foreground backdrop-blur-sm transition-colors hover:bg-muted hover:text-foreground",
        className,
      )}
    >
      <AnimatePresence mode="wait" initial={false}>
        {copied ? (
          <motion.span
            key="check"
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.8 }}
            transition={{ duration: 0.15 }}
            className="flex items-center gap-1"
          >
            <Check className="h-3 w-3" />
            Copied!
          </motion.span>
        ) : (
          <motion.span
            key="copy"
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.8 }}
            transition={{ duration: 0.15 }}
            className="flex items-center gap-1"
          >
            <Copy className="h-3 w-3" />
            Copy
          </motion.span>
        )}
      </AnimatePresence>
    </button>
  )
}

// ── Terminal chrome wrapper ─────────────────────────────────────

export interface TerminalProps {
  title?: string
  children: React.ReactNode
  className?: string
  /** Provide a getter for the copy text. If omitted, the copy button is hidden. */
  getText?: () => string
}

export function Terminal({
  title,
  children,
  className,
  getText,
}: TerminalProps) {
  return (
    <div
      className={cn(
        "rounded-lg border bg-card text-card-foreground overflow-hidden",
        className,
      )}
    >
      <div className="relative flex items-center justify-center border-b bg-muted/50 px-4 py-3">
        <div className="absolute left-4 top-1/2 flex -translate-y-1/2 items-center gap-2">
          <span className="h-3 w-3 rounded-full bg-[#FF5F56]" />
          <span className="h-3 w-3 rounded-full bg-[#FFBD2E]" />
          <span className="h-3 w-3 rounded-full bg-[#27C93F]" />
        </div>
        {title && (
          <span className="text-sm font-medium text-muted-foreground">
            {title}
          </span>
        )}
      </div>
      <div className="relative">
        {getText && <CopyButton getText={getText} />}
        <div className="max-h-[600px] overflow-auto text-sm [&_pre]:p-4 [&_pre]:m-0 [&_pre]:!bg-transparent [&_code]:!bg-transparent">
          {children}
        </div>
      </div>
    </div>
  )
}

// ── Client-side Shiki highlighter ───────────────────────────────

function HighlightedCode({
  code,
  language,
}: {
  code: string
  language: string
}) {
  const ref = React.useRef<HTMLDivElement>(null)
  const [ready, setReady] = React.useState(false)

  React.useEffect(() => {
    let cancelled = false
    setReady(false)
    codeToHtml(code, {
      lang: language,
      themes: {
        light: "github-light",
        dark: "github-dark",
      },
    }).then((result) => {
      if (!cancelled && ref.current) {
        ref.current.innerHTML = result
        setReady(true)
      }
    })
    return () => {
      cancelled = true
    }
  }, [code, language])

  return (
    <>
      <div ref={ref} className={cn(!ready && "hidden")} />
      {!ready && (
        <pre className="p-4">
          <code>{code}</code>
        </pre>
      )}
    </>
  )
}

// ── CodeBlock component ─────────────────────────────────────────

export interface CodeBlockProps {
  code: string
  language?: string
  filename?: string
  showLineNumbers?: boolean
  className?: string
  variant?: "default" | "terminal" | "filename"
}

export function CodeBlock({
  code,
  language = "tsx",
  filename,
  showLineNumbers = false,
  className,
  variant,
}: CodeBlockProps) {
  const effectiveVariant = variant || (filename ? "filename" : "default")
  const getText = React.useCallback(() => code, [code])

  const codeContent = (
    <div
      className={cn(
        showLineNumbers &&
          "[&_.line]:before:mr-4 [&_.line]:before:inline-block [&_.line]:before:w-4 [&_.line]:before:text-right [&_.line]:before:text-muted-foreground [&_.line]:before:content-[counter(line)] [&_.line]:before:[counter-increment:line] [&_pre]:[counter-reset:line]",
      )}
    >
      <HighlightedCode code={code} language={language} />
    </div>
  )

  if (effectiveVariant === "terminal") {
    return (
      <Terminal title={filename} className={className} getText={getText}>
        {codeContent}
      </Terminal>
    )
  }

  return (
    <div
      className={cn(
        "rounded-lg border bg-card text-card-foreground overflow-hidden",
        className,
      )}
    >
      {effectiveVariant === "filename" && (
        <div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2">
          <div className="flex items-center gap-2">
            <FileCode className="h-4 w-4 text-muted-foreground" />
            <span className="text-sm font-medium">{filename}</span>
          </div>
          <span className="text-xs text-muted-foreground">{language}</span>
        </div>
      )}
      <div className="relative">
        <CopyButton getText={getText} />
        <div
          className={cn(
            "max-h-[600px] overflow-auto text-sm [&_pre]:p-4 [&_pre]:m-0 [&_pre]:!bg-transparent [&_code]:!bg-transparent",
            showLineNumbers &&
              "[&_.line]:before:mr-4 [&_.line]:before:inline-block [&_.line]:before:w-4 [&_.line]:before:text-right [&_.line]:before:text-muted-foreground [&_.line]:before:content-[counter(line)] [&_.line]:before:[counter-increment:line] [&_pre]:[counter-reset:line]",
          )}
        >
          <HighlightedCode code={code} language={language} />
        </div>
      </div>
    </div>
  )
}

Examples

Variants

Terminal and filename variants side by side for comparison.