Mikkel Manniche
Dansk English Deutsch

AI agents

Code block

Code with a file name and a copy button. Bring your own highlighting, or none.

Preview

Usage

code-block-demo.tsx
import { CodeBlock } from '@/components/code-block'

const code = `export function total(items: { price: number; qty: number }[]) {
  return items.reduce((sum, i) => sum + i.price * i.qty, 0)
}`

export default function CodeBlockDemo() {
  return <CodeBlock filename="lib/total.ts" code={code} lineNumbers />
}

Source

code-block.tsx
import { Check, Copy } from 'lucide-react'
import { useEffect, useState, type ReactNode } from 'react'
import { cn } from '@/lib/utils'

export type CodeBlockProps = {
  /** The code as text. This is what the copy button copies. */
  code: string
  /** Shown in the header, e.g. "tsx" or "bash". */
  language?: string
  /** Shown in the header instead of the language, e.g. "src/app.tsx". */
  filename?: string
  /** Already highlighted markup (from Shiki or similar). Without it the plain code is shown. */
  children?: ReactNode
  /** Number the lines. */
  lineNumbers?: boolean
  className?: string
}

/**
 * Code with a header and a copy button. It does no highlighting itself, so it adds no weight;
 * pass highlighted markup as children if you want colours.
 */
export function CodeBlock({ code, language, filename, children, lineNumbers = false, className }: CodeBlockProps) {
  const [copied, setCopied] = useState(false)

  useEffect(() => {
    if (!copied) return
    const id = setTimeout(() => setCopied(false), 1800)
    return () => clearTimeout(id)
  }, [copied])

  async function copy() {
    try {
      await navigator.clipboard.writeText(code)
      setCopied(true)
    } catch {
      // The clipboard can be blocked (an insecure page, a denied permission). The code can still be selected by hand.
    }
  }

  const lines = code.replace(/\n$/, '').split('\n')

  return (
    <figure className={cn('overflow-hidden rounded-2xl border bg-card text-card-foreground', className)}>
      <figcaption className="flex items-center justify-between gap-3 border-b bg-muted/50 py-1 pr-1 pl-4">
        <span className="truncate font-mono text-[0.8125rem] text-muted-foreground">{filename ?? language ?? 'Code'}</span>
        <button
          type="button"
          onClick={copy}
          className="inline-flex min-h-11 shrink-0 items-center gap-1.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground"
        >
          {copied ? <Check className="size-4 text-success" aria-hidden /> : <Copy className="size-4" aria-hidden />}
          <span aria-live="polite">{copied ? 'Copied' : 'Copy'}</span>
        </button>
      </figcaption>
      <pre className="max-h-[28rem] overflow-auto py-3 font-mono text-[0.8125rem] leading-6" tabIndex={0}>
        {children ? (
          <div className="px-4">{children}</div>
        ) : lineNumbers ? (
          <code className="grid grid-cols-[auto_1fr]">
            {lines.map((line, i) => (
              <span key={i} className="contents">
                <span className="pr-4 pl-4 text-right text-muted-foreground select-none" aria-hidden>
                  {i + 1}
                </span>
                <span className="pr-4">{line || ' '}</span>
              </span>
            ))}
          </code>
        ) : (
          <code className="block px-4">{code}</code>
        )}
      </pre>
    </figure>
  )
}

Install

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/code-block.json

With the registry added (components.json)

npx shadcn@latest add @manniche/code-block

Files

  • code-block.tsx