Code Block

Syntax-highlighted code display with copy support.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

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

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

"use client"; import { type ComponentType, type ReactNode, useEffect, useRef, useState, } from "react"; import { Check, Copy } from "lucide-react"; import { useTheme } from "next-themes"; import type { SyntaxHighlighterProps } from "react-syntax-highlighter"; import { cn } from "../../lib/utils"; import { Button } from "../button/button"; type PrismStyle = SyntaxHighlighterProps["style"]; type LoadedHighlighter = { oneDark: PrismStyle; oneLight: PrismStyle; SyntaxHighlighter: ComponentType<SyntaxHighlighterProps>; }; 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 ?? ""); } function findScrollableParent( element: HTMLElement | null, ): HTMLElement | undefined { if (!element) return undefined; if (element.scrollHeight > element.clientHeight) return element; return findScrollableParent(element.parentElement); } export function CodeBlock({ children, className, language = "typescript", showLanguage = false, }: CodeBlockProps) { const [copied, setCopied] = useState(false); // react-syntax-highlighter (~10MB) is dynamic-imported on mount so the // @vllnt/ui barrel's static graph never reaches it — barrel consumers that // never render a CodeBlock ship zero bytes of it. Null until the chunk loads. const [highlighter, setHighlighter] = useState<LoadedHighlighter | null>( null, ); const { systemTheme, theme } = useTheme(); const resolvedTheme = theme === "system" ? systemTheme : theme; const isDark = resolvedTheme !== "light"; const code = extractTextFromChildren(children); const scrollRef = useRef<HTMLDivElement>(null); useEffect(() => { let active = true; void Promise.all([ import("react-syntax-highlighter"), import("react-syntax-highlighter/dist/esm/styles/prism"), ]).then(([module_, styles]) => { if (!active) return; setHighlighter({ oneDark: styles.oneDark, oneLight: styles.oneLight, SyntaxHighlighter: module_.Prism, }); }); return () => { active = false; }; }, []); useEffect(() => { const element = scrollRef.current; if (!element) return; const onWheel = (event: WheelEvent) => { if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return; const scrollable = findScrollableParent(element); if (scrollable) { scrollable.scrollTop += event.deltaY; event.preventDefault(); } }; element.addEventListener("wheel", onWheel, { passive: false }); return () => { element.removeEventListener("wheel", onWheel); }; }, []); const handleCopy = async () => { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => { setCopied(false); }, 2000); }; const SyntaxHighlighter = highlighter?.SyntaxHighlighter; const codeStyle = isDark ? highlighter?.oneDark : highlighter?.oneLight; return ( <div className={cn( "relative w-full overflow-hidden rounded-md border bg-background", className, )} > <div className="relative overflow-x-auto overflow-y-hidden touch-pan-y" ref={scrollRef} > {SyntaxHighlighter ? ( <SyntaxHighlighter codeTagProps={{ className: "font-mono text-sm", style: { background: "transparent", display: "block", }, }} customStyle={{ background: "oklch(var(--background))", fontSize: "0.875rem", margin: 0, minWidth: "fit-content", overflowY: "hidden", padding: "1rem", }} language={language} style={codeStyle} > {code} </SyntaxHighlighter> ) : ( <pre className="m-0 min-w-fit overflow-y-hidden p-4 font-mono text-sm"> <code className="block bg-transparent">{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 aria-label={copied ? "Copied" : "Copy code"} className="size-8" onClick={handleCopy} size="icon" variant="ghost" > {copied ? ( <Check className="size-3" /> ) : ( <Copy className="size-3" /> )} </Button> </div> </div> </div> ); }

Dependencies

  • @vllnt/ui@^0.2.1