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}
1"use client";
2
3import { 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";
8
9interface 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}
22
23export 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);
34
35 const lineCount = useMemo(
36 () => code.trim().split(/\r?\n/).length || 0,
37 [code],
38 );
39
40 const handleCopy = useCallback(async () => {
41 await navigator.clipboard.writeText(code);
42 setCopied(true);
43 setTimeout(() => setCopied(false), 2000);
44 }, [code]);
45
46 const highlightBody = (
47 <Highlight theme={themes.nightOwl} code={code.trim()} language={language}>
48 {({ className, style, tokens, getLineProps, getTokenProps }) => (
49 <pre
50 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 <div
58 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 );
84
85 const copyButton = (
86 <button
87 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 );
105
106 return (
107 <div
108 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 <button
117 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 <ChevronDown
121 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 <div
147 className={cn(
148 "relative transition-[max-height] duration-200 ease-out",
149 sourceOpen
150 ? "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 <div
157 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-hidden
159 />
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));
1import { CodeBlock } from "@/registry/ui/code-block"
2
3<CodeBlock
4 language="javascript"
5 showLineNumbers={false}
6 code="const sum = (a, b) => a + b;\nconsole.log(sum(1, 2));"
7/>

Props

NameTypeDefaultDescription
codestringRequiredSource text to highlight and copy
languagestring"typescript"Prism language id for highlighting
filenamestringOptional label in the header (hides language chip when set)
showLineNumbersbooleantrueShow gutter line numbers