Code Block Component

A code block component with syntax highlighting and copy functionality.

import { CodeBlock } from "@/components/ui/code-block"

export function Example() {
  return (
    <CodeBlock language="typescript">
      const greeting = 'Hello, World!'
    </CodeBlock>
  )
}

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.com/r/code-block.json
bash

Usage

import { CodeBlock } from '@vllnt/ui'

export function CodeBlockExample() {
return (
  <CodeBlock language="javascript" showLanguage={true}>
    {\`console.log('Hello, World!')\`}
  </CodeBlock>
)
}
tsx

Code

'use client'

import { type ReactNode, useEffect, useState } from 'react'

import { Check, Copy } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'

import { cn } from '../../lib/utils'
import { Button } from '../button/button'

type CodeBlockProps = {
  children: ReactNode
  className?: string
  language?: string
  showLanguage?: boolean
}

function extractTextFromChildren(children: ReactNode): string {
  if (typeof children === 'string') {
    return children
  }
  if (typeof children === 'number') {
    return String(children)
  }
  if (Array.isArray(children)) {
    return children.map(extractTextFromChildren).join('')
  }
  if (
    children &&
    typeof children === 'object' &&
    'props' in children &&
    children.props &&
    typeof children.props === 'object' &&
    'children' in children.props
  ) {
    return extractTextFromChildren(children.props.children as ReactNode)
  }
  return String(children ?? '')
}

// eslint-disable-next-line max-lines-per-function
export function CodeBlock({
  children,
  className,
  language = 'typescript',
  showLanguage = false,
}: CodeBlockProps) {
  const [copied, setCopied] = useState(false)
  const [mounted, setMounted] = useState(false)
  const { systemTheme, theme } = useTheme()

  useEffect(() => {
    setMounted(true)
  }, [])

  const resolvedTheme = theme === 'system' ? systemTheme : theme
  const isDark = resolvedTheme === 'dark'
  const codeStyle = isDark ? oneDark : oneLight
  const code = extractTextFromChildren(children)

  const handleCopy = async () => {
    await navigator.clipboard.writeText(code)
    setCopied(true)
    setTimeout(() => {
      setCopied(false)
    }, 2000)
  }

  // Prevent hydration mismatch by not rendering until mounted
  if (!mounted) {
    return (
      <div
        className={cn('relative w-full overflow-hidden rounded-md border bg-background', className)}
      >
        <div className="relative overflow-x-auto">
          <pre className="p-4 text-sm font-mono bg-background">
            <code>{code}</code>
          </pre>
          <div className="absolute right-2 top-2 flex items-center gap-2">
            {showLanguage ? (
              <span className="text-xs font-mono text-muted-foreground uppercase tracking-wider">
                {language}
              </span>
            ) : null}
            <Button className="h-8 w-8" onClick={handleCopy} size="icon" variant="ghost">
              <Copy className="h-3 w-3" />
            </Button>
          </div>
        </div>
      </div>
    )
  }

  return (
    <div
      className={cn('relative w-full overflow-hidden rounded-md border bg-background', className)}
    >
      <div className="relative overflow-x-auto">
        <SyntaxHighlighter
          codeTagProps={{
            className: 'font-mono text-sm',
          }}
          customStyle={{
            background: 'hsl(var(--background))',
            fontSize: '0.875rem',
            margin: 0,
            minWidth: 'fit-content',
            padding: '1rem',
          }}
          language={language}
          style={codeStyle}
        >
          {code}
        </SyntaxHighlighter>
        <div className="absolute right-2 top-2 flex items-center gap-2">
          {showLanguage ? (
            <span className="text-xs font-mono text-muted-foreground uppercase tracking-wider">
              {language}
            </span>
          ) : null}
          <Button className="h-8 w-8" onClick={handleCopy} size="icon" variant="ghost">
            {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
          </Button>
        </div>
      </div>
    </div>
  )
}
typescript

Dependencies

  • next-themes
  • react-syntax-highlighter
  • clsx
  • tailwind-merge