Display15
Form1
Playground3
Code Block
Syntax-highlighted code with optional filename, line numbers, and copy.
Default
TypeScript sample with filename and line numbers
example.ts
1export function greet(name: string) {2 return `Hello, ${name}!`;3}
code-block.tsx
1"use client";23import { useState, useCallback, useMemo } from "react";4import { Highlight, themes } from "prism-react-renderer";5import { Check, ChevronDown, Copy } from "lucide-react";6import { cn } from "@/lib/utils";7import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible";89interface CodeBlockProps {10 code: string;11 language?: string;12 filename?: string;13 showLineNumbers?: boolean;14 /**15 * When true, the body starts in a short preview (clipped + fade); expand for full scrollable source.16 */17 collapsible?: boolean;18 /** Bump when the Code tab is opened again so the block returns to collapsed. */19 collapseResetKey?: number;20 showCopyButton?: boolean;21}2223export function CodeBlock({24 code,25 language = "typescript",26 filename,27 showLineNumbers = true,28 collapsible = false,29 collapseResetKey = 0,30 showCopyButton = true,31}: CodeBlockProps) {32 const [copied, setCopied] = useState(false);33 const [sourceOpen, setSourceOpen] = useState(!collapsible);3435 const lineCount = useMemo(36 () => code.trim().split(/\r?\n/).length || 0,37 [code],38 );3940 const handleCopy = useCallback(async () => {41 await navigator.clipboard.writeText(code);42 setCopied(true);43 setTimeout(() => setCopied(false), 2000);44 }, [code]);4546 const highlightBody = (47 <Highlight theme={themes.nightOwl} code={code.trim()} language={language}>48 {({ className, style, tokens, getLineProps, getTokenProps }) => (49 <pre50 className={`${className} p-4 overflow-x-auto`}51 style={{ ...style, backgroundColor: "transparent", margin: 0 }}52 >53 {tokens.map((line, i) => {54 const lineProps = getLineProps({ line, key: i });55 const { key: _lineKey, ...lineRest } = lineProps;56 return (57 <div58 key={i}59 {...lineRest}60 className={`${lineRest.className || ""} table-row`}61 >62 {showLineNumbers && (63 <span className="table-cell w-8 select-none pr-4 text-right text-muted-foreground/50">64 {i + 1}65 </span>66 )}67 <span className="table-cell">68 {line.map((token, tokenIndex) => {69 const tokenProps = getTokenProps({70 token,71 key: tokenIndex,72 });73 const { key: _tokenKey, ...tokenRest } = tokenProps;74 return <span key={tokenIndex} {...tokenRest} />;75 })}76 </span>77 </div>78 );79 })}80 </pre>81 )}82 </Highlight>83 );8485 const copyButton = (86 <button87 type="button"88 onClick={handleCopy}89 className="flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"90 aria-label={copied ? "Copied" : "Copy code"}91 >92 {copied ? (93 <>94 <Check className="size-4 text-terminal-green" />95 <span className="text-xs text-terminal-green">Copied</span>96 </>97 ) : (98 <>99 <Copy className="size-4" />100 <span className="text-xs">Copy</span>101 </>102 )}103 </button>104 );105106 return (107 <div108 key={collapsible ? `code-block-${collapseResetKey}` : undefined}109 className="overflow-hidden rounded-lg border border-border bg-card font-mono text-sm"110 >111 {collapsible ? (112 <Collapsible open={sourceOpen} onOpenChange={setSourceOpen}>113 <div className="flex items-center justify-between gap-2 border-b border-border bg-secondary/50 px-4 py-2">114 <div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">115 <CollapsibleTrigger asChild>116 <button117 type="button"118 className="flex max-w-full min-w-0 items-center gap-1.5 rounded-md px-1.5 py-1 text-left text-sm font-medium text-foreground hover:bg-secondary/80"119 >120 <ChevronDown121 className={cn(122 "size-4 shrink-0 text-muted-foreground transition-transform",123 sourceOpen && "rotate-180",124 )}125 />126 <span className="truncate">127 {sourceOpen ? "Show less" : "Show all"}128 </span>129 <span className="shrink-0 text-xs font-normal text-muted-foreground">130 ({lineCount} lines)131 </span>132 </button>133 </CollapsibleTrigger>134 {filename ? (135 <span className="hidden truncate font-medium text-foreground/90 sm:inline">136 {filename}137 </span>138 ) : language ? (139 <span className="hidden text-xs uppercase tracking-wider text-muted-foreground sm:inline">140 {language}141 </span>142 ) : null}143 </div>144 {showCopyButton && copyButton}145 </div>146 <div147 className={cn(148 "relative transition-[max-height] duration-200 ease-out",149 sourceOpen150 ? "max-h-[min(85vh,56rem)] overflow-y-auto overflow-x-auto"151 : "max-h-[13rem] overflow-hidden",152 )}153 >154 {highlightBody}155 {!sourceOpen && lineCount > 1 ? (156 <div157 className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-gradient-to-t from-card from-40% via-card/70 to-transparent"158 aria-hidden159 />160 ) : null}161 </div>162 </Collapsible>163 ) : (164 <>165 <div className="flex items-center justify-between border-b border-border bg-secondary/50 px-4 py-2">166 <div className="flex items-center gap-3">167 {filename && (168 <span className="font-medium text-foreground/90">169 {filename}170 </span>171 )}172 {!filename && language && (173 <span className="text-xs uppercase tracking-wider text-muted-foreground">174 {language}175 </span>176 )}177 </div>178 {showCopyButton && copyButton}179 </div>180 {highlightBody}181 </>182 )}183 </div>184 );185}
JavaScript without line numbers
No filename and no line numbers
javascript
const sum = (a, b) => a + b;console.log(sum(1, 2));
tsx
1import { CodeBlock } from "@/registry/ui/code-block"23<CodeBlock4 language="javascript"5 showLineNumbers={false}6 code="const sum = (a, b) => a + b;\nconsole.log(sum(1, 2));"7/>
Props
| Name | Type | Default | Description |
|---|---|---|---|
| code | string | Required | Source text to highlight and copy |
| language | string | "typescript" | Prism language id for highlighting |
| filename | string | — | Optional label in the header (hides language chip when set) |
| showLineNumbers | boolean | true | Show gutter line numbers |