Custom Shadcn Code Block for React and Tailwind CSS. A shadcn code block with Shiki syntax highlighting, streaming, diffs, code folding and per-line interaction for AI chat and agent UIs.
The only npm dependency is shiki. Grammars and themes load lazily, one chunk
per language, and nothing loads at all when you pass pre-highlighted lines or
turn highlighting off.
Usage
import { CodeBlock, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLanguage, CodeBlockTitle,} from "@/components/reui/code-block/code-block"
Shadcn Code Block Free Components
Browse 27 production-ready Shadcn Code Block components for dashboards, forms, and product UI. These examples follow the Radix UI implementation with accessible primitives from the Radix stack and stay fully compatible with Shadcn Create so radius, color, and typography match your configured theme.
CodeBlock renders the code surface itself; with the default surface,
children are chrome (a header, a copy button, an expand control) and their
order never matters. Composing CodeBlockContent makes the surface one of
the children, and from then on order is DOM order.
<CodeBlock> <CodeBlockHeader> <CodeBlockTitle /> <CodeBlockLanguage /> <CodeBlockWrapToggle /> <CodeBlockCopyButton /> </CodeBlockHeader> <CodeBlockContent /> {/* optional: compose inside your own ScrollArea */} <CodeBlockExpandButton /> <CodeBlockLineActions>{({ line }) => null}</CodeBlockLineActions></CodeBlock>
A CodeBlockCopyButton inside a header is a normal flex child. Outside one it
pins itself over the surface and stays put under both vertical and horizontal
scrolling.
Framing
The default variant draws a bordered surface. Use variant="ghost" when
something else already provides the chrome, so you do not get a doubled
border. Ghost keeps the block's own padding; a flush fit is the container's
job (p-0 on the panel, as the framed examples do).
"use client"import { CodeBlock, CodeBlockCopyButton,} from "@/components/reui/code-block/code-block"import { Frame, FrameHeader, FramePanel, FrameTitle,} from "@/components/reui/frame"import { Tabs, TabsContent, TabsList, TabsTrigger,} from "@/components/ui/tabs"const samples = [ { file: "greeting.tsx", language: "tsx", code: `export function Greeting({ name }: { name: string }) { return <p>Hello {name}</p>}`, }, { file: "greeting.py", language: "python", code: `def greeting(name: str) -> str: return f"Hello {name}"`, }, { file: "greeting.sql", language: "sql", code: `select id, emailfrom userswhere created_at > now() - interval '7 days';`, }, { file: "greeting.sh", language: "bash", code: `curl -s https://api.example.com/v1/users \\ -H "Authorization: Bearer $TOKEN"`, },]/* * One grammar chunk loads per selected language, so switching files is also a * lazy-loading demo. * * A dense frame is what makes this read as one editor: the panel meets the * header with no gap or inset, and the ghost block inside contributes no * second border. The tabs live in the header, so the blocks stay headerless * and the copy button pins itself over the code. */export function Pattern() { return ( <Tabs defaultValue={samples[0].file} className="w-full max-w-2xl"> <Frame dense spacing="sm"> <FrameHeader className="flex-row items-center gap-2"> <FrameTitle>Examples</FrameTitle> <TabsList className="ml-auto bg-transparent"> {samples.map((sample) => ( <TabsTrigger key={sample.file} value={sample.file}> {sample.file} </TabsTrigger> ))} </TabsList> </FrameHeader> <FramePanel className="p-0!"> {samples.map((sample) => ( <TabsContent key={sample.file} value={sample.file}> <CodeBlock code={sample.code} language={sample.language} variant="ghost" showLineNumbers > <CodeBlockCopyButton variant="outline" size="icon-xs" className="bg-card hover:bg-muted" /> </CodeBlock> </TabsContent> ))} </FramePanel> </Frame> </Tabs> )}
Scrolling
CodeBlockContent is the code surface as a composable part: place it inside
your own scroll container and the block stops scrolling internally - the
ancestor owns both axes.
By default the block scrolls with a plain overflow: auto container of its
own - no ScrollArea dependency, native thin scrollbars, and maxLines caps
it.
To own the scrolling yourself, compose CodeBlockContent inside your own
scroll container. The ancestor takes BOTH axes - the surface scrolls
nothing itself - and a header above the area never scrolls away. The sticky
line numbers anchor to the ancestor's viewport, and stick-to-bottom during
streaming follows it automatically. Pass a ScrollBar orientation="horizontal"
child for the horizontal bar; Base UI positions it against the scroll area's
root, so it stays pinned even though it sits among the children.
Use max-h-*, not a fixed height, so a short file does not leave dead space,
and put it on the ScrollArea VIEWPORT (as above): the viewport is height-100%
of the root, and a percentage against a max-height-only parent resolves to
auto, so a root-level cap clips without ever scrolling.
maxLines still marks the block collapsible, so CodeBlockExpandButton
works either way; only the height cap itself belongs to the built-in
viewport. To collapse a composed surface, read expanded from
useCodeBlockConfig and cap your own ScrollArea, as the expand example does.
CodeBlockContent must appear in the JSX you pass to CodeBlock - any
depth, any wrapper, your own components included. It is invisible only when a
child component CONSTRUCTS it internally instead of receiving it as children,
and development logs an error if that happens.
import { CodeBlock, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/reui/code-block/code-block"import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"const code = `export function useTheme() { const [theme, setTheme] = useState<"light" | "dark">("light") const toggle = () => setTheme((value) => (value === "light" ? "dark" : "light")) return { theme, toggle }}`/* * The "css-variables" theme emits var(--code-token-*) references instead of * hex colors, so the palette below IS the syntax theme: swap these lines for * your design tokens and the highlighting follows your brand in both modes. */const paletteClass = [ "[--code-token-keyword:var(--color-rose-600)]", "[--code-token-function:var(--color-violet-600)]", "[--code-token-string:var(--color-emerald-600)]", "[--code-token-string-expression:var(--color-emerald-600)]", "[--code-token-constant:var(--color-sky-600)]", "[--code-token-parameter:var(--color-amber-600)]", "[--code-token-comment:var(--muted-foreground)]", "[--code-token-punctuation:var(--foreground)]", "[--code-foreground:var(--foreground)]", "dark:[--code-token-keyword:var(--color-rose-400)]", "dark:[--code-token-function:var(--color-violet-400)]", "dark:[--code-token-string:var(--color-emerald-400)]", "dark:[--code-token-string-expression:var(--color-emerald-400)]", "dark:[--code-token-constant:var(--color-sky-400)]", "dark:[--code-token-parameter:var(--color-amber-400)]",].join(" ")export function Pattern() { return ( <CodeBlock code={code} language="typescript" showLineNumbers themes={{ light: "css-variables", dark: "css-variables" }} className={"w-full max-w-2xl " + paletteClass} > <CodeBlockHeader> <CodeBlockTitle>use-theme.ts</CodeBlockTitle> <div className="ml-auto flex items-center gap-1.5"> <span className="text-muted-foreground text-xs"> Colors from design tokens </span> <CodeBlockCopyButton /> </div> </CodeBlockHeader> {/* The ScrollArea composes INSIDE the block, so the block's own border and per-style radius stay the container, the header above never scrolls, and `max-h` means the area grows to the content instead of stretching past a short file. */} <ScrollArea className="rounded-[inherit] **:data-[slot=scroll-area-viewport]:max-h-72"> <CodeBlockContent /> <ScrollBar orientation="horizontal" /> </ScrollArea> </CodeBlock> )}
The inverse composition also works - default-mode blocks that size to content,
listed inside one outer ScrollArea that scrolls the whole set - and ships as a
changed-file list on the
Code Block components page.
Server rendering
highlightCode lives in its own module with no "use client" directive, so a
server component can call it. The result is plain JSON, so it crosses the
boundary as data and the browser never downloads a grammar.
lines is a plain array, so any highlighter can produce it. If you already
use Prism or highlight.js, map its output to CodeBlockLine[] and the
component never loads shiki.
Streaming and AI SDK
Pass a code string that grows. The component re-tokenizes on a deferred
value, and the highlighter returns the same line objects for lines that did not
change, so an appended chunk re-renders one line rather than the whole file.
Lines past what the highlighter has caught up to render as plain text, so the
newest token is visible immediately.
streaming is presentational: it shows an underline caret, sticks the
viewport to the bottom until the reader scrolls up, sets aria-busy, and
turns on motion. New lines slide in once; tokens deliberately do not animate,
because the highlighter runs a chunk behind the stream and replaces plain
lines with tokenized ones - colour arriving instantly reads as highlighting,
while a mount-keyed fade re-flashed lines that were already readable. The
animation is compositor-only and honours prefers-reduced-motion.
Incremental rendering is always memoized, so forgetting the prop costs
correctness nothing.
"use client"import { useEffect, useState } from "react"import { Badge } from "@/components/reui/badge"import { CodeBlock, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLanguage, CodeBlockTitle,} from "@/components/reui/code-block/code-block"import { Button } from "@/components/ui/button"import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"/* * The sample deliberately omits its import statements: the registry verifier * scans raw file text for import shapes and would read them as real package * dependencies of the example, which then fails `registry:verify`. */const target = `export async function POST(request: Request) { const { messages } = await request.json() const result = streamText({ model: openai("gpt-5"), system: "You are a concise assistant.", messages, temperature: 0.2, maxOutputTokens: 1024, }) result.usage.then((usage) => { metrics.record("chat.tokens", usage.totalTokens) }) return result.toUIMessageStreamResponse()}`export function Pattern() { const [length, setLength] = useState(0) useEffect(() => { if (length >= target.length) return const id = window.setTimeout(() => setLength((value) => value + 3), 24) return () => window.clearTimeout(id) }, [length]) const done = length >= target.length return ( <div className="flex w-full max-w-2xl flex-col gap-3"> <CodeBlock code={target.slice(0, length)} language="typescript" showLineNumbers streaming={!done} > <CodeBlockHeader> <CodeBlockTitle>app/api/chat/route.ts</CodeBlockTitle> <CodeBlockLanguage /> <div className="ml-auto flex items-center gap-1.5"> <Badge variant={done ? "success-light" : "info-light"}> {done ? "Complete" : "Generating"} </Badge> <CodeBlockCopyButton /> </div> </CodeBlockHeader> {/* The consumer's ScrollArea owns the scroll, and stick-to-bottom follows it: the primitive resolves the nearest scrolling ancestor per chunk, so the caret stays in view exactly as it does with the built-in viewport. */} <ScrollArea className="rounded-[inherit] **:data-[slot=scroll-area-viewport]:max-h-60"> <CodeBlockContent /> <ScrollBar orientation="horizontal" /> </ScrollArea> </CodeBlock> <Button variant="outline" size="sm" className="self-start" onClick={() => setLength(0)} > Replay stream </Button> </div> )}
Markdown code fences
Most AI chat UIs do not hold a raw code string. They hold a markdown message
with fenced blocks in it. Two helpers cover both routes.
Rendering a message yourself, markdownFences splits it into prose and code
and reports a fence whose closing delimiter has not streamed in yet:
An unknown language and an unterminated fence both render as plain text rather
than throwing, which is what a half-streamed message always looks like.
"use client"import { useEffect, useState } from "react"import { CodeBlock, CodeBlockCopyButton, markdownFences,} from "@/components/reui/code-block/code-block"import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle,} from "@/components/reui/frame"import { Avatar, AvatarFallback, AvatarImage,} from "@/components/ui/avatar"const reply = `Use the streaming helper, then render it:\`\`\`tsxconst { messages } = useChat({ api: "/api/chat" })\`\`\`That keeps the transcript in sync.`/* * `markdownFences` flags a fence whose closing delimiter has not arrived as * `open`, which is what lets the transcript render the partial block mid * stream instead of dropping it until the closer lands. */export function Pattern() { const [length, setLength] = useState(0) useEffect(() => { if (length >= reply.length) return const id = window.setTimeout(() => setLength((value) => value + 4), 30) return () => window.clearTimeout(id) }, [length]) const streamed = reply.slice(0, length) const streaming = length < reply.length return ( <Frame dense className="w-full max-w-2xl"> <FrameHeader> <FrameTitle>Assistant</FrameTitle> <FrameDescription> Generated responses may contain mistakes. </FrameDescription> </FrameHeader> <FramePanel className="flex flex-col gap-4"> <div className="flex gap-3"> <Avatar className="size-7 shrink-0 rounded-full"> <AvatarImage src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80" alt="Mira Stone" /> <AvatarFallback className="text-[10px] font-medium"> MS </AvatarFallback> </Avatar> <p className="pt-1 text-sm">How do I stream chat messages?</p> </div> <div className="flex gap-3"> <Avatar className="size-7 shrink-0 rounded-full"> <AvatarFallback className="bg-muted text-muted-foreground text-[10px] font-medium"> AI </AvatarFallback> </Avatar> <div className="flex min-w-0 flex-1 flex-col gap-2 pt-1"> {markdownFences(streamed).map((part, index) => part.type === "text" ? ( <p key={index} className="text-sm whitespace-pre-wrap"> {part.content} </p> ) : ( <CodeBlock key={index} code={part.content} language={part.language} streaming={streaming && part.open} > <CodeBlockCopyButton /> </CodeBlock> ) )} </div> </div> </FramePanel> </Frame> )}
Bundle and CSP
The highlighter uses shiki's JavaScript regex engine, not oniguruma. There is
no WebAssembly, so installing this does not force 'wasm-unsafe-eval' into
your Content Security Policy.
shiki loads on first highlight, never on import.
Each language is its own chunk, resolved through a static map.
highlight={false} and the lines prop never request any of it.
Adding a language
Languages are a written-out map in code-block-highlight.tsx. Add a line:
Aliases resolve first, so shorthands live one map up: add ex: "elixir" to
LANGUAGE_ALIASES and both names work. An entry does not have to come from
shiki either - mylang: () => import("./my-grammar.json") registers a custom
TextMate grammar, since the loader feeds loadLanguage directly. Anything
unresolved renders as plain text.
The map is written out on purpose. Building the specifier from a template
literal instead makes bundlers emit a context module containing every grammar
shiki ships, which quietly turns a lean install into a very large one.
Theming
Three levels, from zero effort to full control:
Defaults. Blocks ship with github-light / github-dark and switch with
your site theme automatically.
Any shiki theme. Register it in codeBlockThemes in the copied
code-block-highlight.tsx (one line, lazy-loaded), then pass
themes={{ light: "vitesse-light", dark: "vitesse-dark" }}. Changing the
two defaults in that file restyles every block at once. An unregistered
name falls back to its side's default and warns in development - check the
codeBlockThemes entry first when a block renders in the wrong palette.
Design tokens. Pass the built-in css-variables theme and the palette
moves into your stylesheet: every token colour becomes a
var(--code-token-*) reference, so highlighting follows your design system
in both modes with no theme JSON at all.
:root { --code-token-keyword: var(--color-rose-600); --code-token-function: var(--color-violet-600); --code-token-string: var(--color-emerald-600); --code-token-constant: var(--color-sky-600); --code-token-parameter: var(--color-amber-600); --code-token-comment: var(--color-muted-foreground); --code-token-punctuation: var(--color-foreground);}.dark { --code-token-keyword: var(--color-rose-400); /* ...and so on for the dark side of each token */}
The full variable set: --code-foreground, and
--code-token-constant, -string, -comment, -keyword, -parameter,
-function, -string-expression, -punctuation, -link.
Blocks with different themes props coexist on one page; each theme loads
once, by name, on first use.
import { CodeBlock, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/reui/code-block/code-block"const code = `export function Hero({ headline, snippet }: HeroProps) { const seats = useSeatCount() return ( <section className="dark bg-background py-24"> <h1 className="text-4xl font-semibold">{headline}</h1> <img src={cover} /> <pre className="overflow-x-auto">{snippet}</pre> <CodeBlock code={snippet} language="tsx" showLineNumbers /> <a onClick={goToPricing}>Start free</a> <p>{seats} seats claimed</p> </section> )}`/* * A block that stays dark in both site themes, the way marketing pages and * landing heroes usually want code to read. The `dark` class on the wrapper * re-scopes every semantic token inside it, so the primitive needs nothing: * the highlight, both diff tints, the three diagnostic levels and the word * mark all resolve against the dark palette here instead of the page's. */export function Pattern() { return ( <div className="dark w-full max-w-2xl"> <CodeBlock code={code} language="tsx" showLineNumbers highlightedLines={[5]} highlightedWords={["useSeatCount"]} diff={{ removed: [8], added: [9] }} lineLevels={{ error: [10], warning: [7], info: [11] }} > <CodeBlockHeader> <span aria-hidden="true" className="flex shrink-0 items-center gap-1.5" > <span className="size-2.5 rounded-full bg-[#ff5f57]" /> <span className="size-2.5 rounded-full bg-[#febc2e]" /> <span className="size-2.5 rounded-full bg-[#28c840]" /> </span> <CodeBlockTitle className="ml-1">hero.tsx</CodeBlockTitle> <CodeBlockCopyButton className="ml-auto" /> </CodeBlockHeader> </CodeBlock> </div> )}
Examples
A representative six. Every example on this page installs the same way, and
the rest stay browsable on the Code Block components page.
Pinned copy button
import { CodeBlock, CodeBlockContent, CodeBlockCopyButton,} from "@/components/reui/code-block/code-block"import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"const code = `export async function resolveWorkspaceMembershipForActiveSubscription(workspaceId: string, userId: string) { const membership = await db.membership.findFirst({ where: { workspaceId, userId, status: "active" } }) if (!membership) throw new WorkspaceAccessError("No active membership for this workspace") return membership}export function formatSeatSummary(seats: number, used: number) { return \`\${used} of \${seats} seats in use, \${Math.max(0, seats - used)} remaining\`}export function countBillableSeats(members: Member[]) { return members.filter((member) => member.status === "active" && !member.isGuest).length}export function canInviteMember(seats: number, used: number) { return used < seats}`/** * Both axes belong to the consumer's ScrollArea here: `CodeBlockContent` * hands the surface over without an internal scroll container, and the block * itself stays the bordered chrome. `max-h` rather than a fixed height, so a * short file does not leave dead space under the code. * * With no header the copy button pins itself over the code surface, hidden * until the block is hovered or the button takes focus. Line 1 scrolls * underneath it, so the button needs a backdrop of its own: `outline` draws * the border, and `bg-card` fills it (the variant's own dark fill is 4.5% * opaque, which the code shows through). */export function Pattern() { return ( <CodeBlock code={code} language="typescript" className="w-full max-w-2xl"> <CodeBlockCopyButton variant="outline" size="icon-sm" className="bg-card hover:bg-muted" /> {/* The cap goes on the ScrollArea VIEWPORT, not its root: the viewport is height-100% of the root, and a percentage against a max-height only parent resolves to auto, so a root-level cap clips without ever scrolling. */} <ScrollArea className="rounded-[inherit] **:data-[slot=scroll-area-viewport]:max-h-56"> <CodeBlockContent /> <ScrollBar orientation="horizontal" /> </ScrollArea> </CodeBlock> )}
Diff
Line state comes straight from props here - the simplest form. For a real
patch, see the unified parse below.
Regions come from indentation rather than the grammar, so folding works in
every language the block can render. Folding an outer region swallows the
inner toggles and leaves their own state untouched underneath.
"use client"import { useState } from "react"import { CodeBlock, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/reui/code-block/code-block"import { Button } from "@/components/ui/button"import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"import { Separator } from "@/components/ui/separator"const code = `export async function syncWorkspace(workspaceId: string) { const workspace = await db.workspace.findUnique({ where: { id: workspaceId } }) if (!workspace) { throw new WorkspaceNotFoundError(workspaceId) } const members = await db.member.findMany({ where: { workspaceId } }) for (const member of members) { if (member.status === "invited") { await mail.send({ to: member.email, template: "workspace-reminder", }) } } return { synced: members.length }}`/* * The block detects its own regions from indentation, so the only reason this * list exists is the "Fold all" button: the snippet is a literal here, so its * region starts are known without asking the primitive for them. * * The ScrollArea composes INSIDE the block, under the header, so the fold * controls never scroll away and the block's own chrome stays the container. */const REGION_STARTS = [1, 4, 10, 11, 12]export function Pattern() { /* Opens with the inner branch folded, so the nesting is visible at a glance: folding line 10 swallows it, and unfolding 10 brings it back still folded. */ const [folded, setFolded] = useState<number[]>([11]) return ( <CodeBlock code={code} language="typescript" showLineNumbers className="w-full max-w-2xl" foldable folded={folded} onFoldedChange={setFolded} > <CodeBlockHeader className="gap-1.5"> <CodeBlockTitle>sync-workspace.ts</CodeBlockTitle> <div className="ml-auto flex items-center gap-1"> <Button size="xs" variant="ghost" onClick={() => setFolded(REGION_STARTS)} > Fold all </Button> <Button size="xs" variant="ghost" disabled={!folded.length} onClick={() => setFolded([])} > Unfold all </Button> <Separator orientation="vertical" className="h-4" /> <CodeBlockCopyButton /> </div> </CodeBlockHeader> <ScrollArea className="rounded-[inherit] **:data-[slot=scroll-area-viewport]:max-h-72"> <CodeBlockContent /> <ScrollBar orientation="horizontal" /> </ScrollArea> </CodeBlock> )}
Fix with AI on the failing line
The render prop receives each line's state, so one action group serves the
whole block and still only offers the fix where a diagnostic actually sits.
"use client"import { useState } from "react"import { Badge } from "@/components/reui/badge"import { CodeBlock, CodeBlockHeader, CodeBlockLineActions, CodeBlockTitle,} from "@/components/reui/code-block/code-block"import { Button } from "@/components/ui/button"import { SparklesIcon } from 'lucide-react'const broken = `export function totalDue(invoice: Invoice) { const lines = invoice.lines.map((line) => line.amount) const subtotal = lines.reduce((sum, amount) => sum + amount) return subtotal + invoice.tax}`const patched = `export function totalDue(invoice: Invoice) { const lines = invoice.lines.map((line) => line.amount) const subtotal = lines.reduce((sum, amount) => sum + amount, 0) return subtotal + invoice.tax}`export function Pattern() { const [fixed, setFixed] = useState(false) return ( <div className="w-full max-w-2xl"> <CodeBlock code={fixed ? patched : broken} language="typescript" showLineNumbers lineLevels={fixed ? undefined : { error: [3] }} > <CodeBlockHeader> <CodeBlockTitle>invoice.ts</CodeBlockTitle> <div className="ml-auto flex items-center gap-1.5"> {fixed ? ( <> <Badge variant="success-light">Patch applied</Badge> <Button size="xs" variant="ghost" onClick={() => setFixed(false)} > Undo </Button> </> ) : ( <Badge variant="destructive-light"> Empty array crashes reduce </Badge> )} </div> </CodeBlockHeader> {/* The action is scoped to the line that actually carries the diagnostic: the render prop receives that line's state, so one group serves the whole block and still only offers the fix where there is something to fix. */} <CodeBlockLineActions> {({ state }) => state?.level === "error" ? ( <Button size="xs" onClick={() => setFixed(true)}> <SparklesIcon /> Fix with AI </Button> ) : null } </CodeBlockLineActions> </CodeBlock> </div> )}
Reference lines in chat
side="gutter" moves the control into the channel beside the line number,
where an editor puts it. Pressing it turns the hovered line, or the current
selection via useCodeBlockSelection, into file:line reference badges in
the composer below.
Unified patch review
parseUnifiedDiff turns a git patch into per-file lines: tints, +/-
glyphs and the dual old/new gutter numbers all come from the parse, and the
download button saves each file's patch.
import { Badge } from "@/components/reui/badge"import { CodeBlock, CodeBlockDownloadButton, CodeBlockHeader, CodeBlockTitle, parseUnifiedDiff,} from "@/components/reui/code-block/code-block"const patch = `diff --git a/lib/seats.ts b/lib/seats.ts--- a/lib/seats.ts+++ b/lib/seats.ts@@ -12,7 +12,8 @@ export async function countBillableSeats(workspaceId: string) { const members = await db.member.findMany({ where: { workspaceId } })- const used = members.length+ const used = members.filter((member) => !member.isGuest).length+ if (used < 0) throw new SeatCountError(workspaceId) return { used, total: await seatAllowance(workspaceId) } }diff --git a/app/api/invites/route.ts b/app/api/invites/route.ts--- a/app/api/invites/route.ts+++ b/app/api/invites/route.ts@@ -3,6 +3,7 @@ export async function POST(request: Request) { const { email } = await request.json()+ if (!email) return new Response("email required", { status: 422 }) const invite = await createInvite(session.workspaceId, email) return Response.json(invite, { status: 201 }) }`/* * The whole review renders from ONE `parseUnifiedDiff` call: tints, +/- * glyphs and the dual old/new gutter numbers all come out of the parse, so * nothing here counts lines against a hand-concatenated string. The parsed * lines go in through the `lines` prop, which also means no highlighter runs. */const files = parseUnifiedDiff(patch)export function Pattern() { return ( <div className="flex w-full max-w-2xl flex-col gap-3"> {files.map((file) => ( <CodeBlock key={file.file} lines={file.lines} showLineNumbers label={`Patch for ${file.file}`} > <CodeBlockHeader> <CodeBlockTitle>{file.file}</CodeBlockTitle> <div className="ml-auto flex items-center gap-1.5"> <Badge variant="success-light">+{file.added}</Badge> <Badge variant="destructive-light">-{file.removed}</Badge> <CodeBlockDownloadButton filename={`${file.file.split("/").pop()}.patch`} /> </div> </CodeBlockHeader> </CodeBlock> ))} </div> )}
The rest
The other fifteen ship exactly as these do and stay browsable on the
Code Block components page: ANSI terminal output,
a tool-call payload, live search with a match counter, focus walkthroughs,
line selection, soft wrap, an expand control over a long file, pre-highlighted
server lines, before-and-after panes, a language switcher, a shiki transformer
diff, highlighted words, a dark terminal, patch review with accept and reject
actions, and a changed-file list inside one scroll area.
API Reference
CodeBlock
The root. Renders the code surface; children are chrome.
Prop
Type
Default
Description
code
string
-
The source to render.
language
string
-
Grammar to use. Unknown values render as plain text.
lines
CodeBlockLine[]
-
Pre-highlighted lines. Skips the client highlighter entirely.
highlight
boolean
true
false renders plain text and never loads shiki.
themes
{ light: string; dark: string }
github-light / github-dark
Theme pair.
showLineNumbers
boolean
false
Renders the gutter as a CSS counter.
startLine
number
1
First displayed line number.
wrap
boolean
-
Soft wrap, controlled.
defaultWrap
boolean
false
Soft wrap, uncontrolled.
onWrapChange
(wrap: boolean) => void
-
Fired when wrap changes.
maxLines
number
-
Collapses taller content.
variant
"default" | "ghost"
"default"
ghost removes the border and background; padding stays.
label
string
derived
Accessible name for the scroll region.
highlightedLines
number[] | string
-
Source lines to mark, as [2,3] or "2-4,7".
highlightedWords
(string | { word, lines })[]
-
Words to mark.
focusedLines
number[] | string
-
Lines to keep sharp; the rest dim.
diff
{ added?, removed? }
-
Diff lines.
lineLevels
{ error?, warning?, info? }
-
Diagnostic lines.
transformers
ShikiTransformer[]
-
Passed to shiki. Line classes become line state.
streaming
boolean
false
Caret, stick-to-bottom, aria-busy.
expanded
boolean
-
Collapsed state under maxLines, controlled.
defaultExpanded
boolean
false
Collapsed state, uncontrolled.
onExpandedChange
(expanded: boolean) => void
-
Fired when the expand control toggles.
completeAnnouncement
string
derived
Screen-reader text when a stream finishes, for localisation.
selectable
boolean
false
Enables line selection.
foldRegions
CodeBlockFoldRegion[]
derived
Replaces the indentation heuristic with your own regions.
foldable
boolean
false
Detects fold regions from indentation and renders a toggle per region.
folded
number[]
-
Folded regions by start line, controlled.
defaultFolded
number[]
[]
Folded regions by start line, uncontrolled.
onFoldedChange
(folded: number[]) => void
-
Fired when a region folds or unfolds.
selectedLines
number[]
-
Selected lines, controlled.
defaultSelectedLines
number[]
[]
Selected lines, uncontrolled.
onSelectedLinesChange
(lines: number[]) => void
-
Fired on selection change.
Line specs address source lines, 1-based within code, so changing startLine
never invalidates them.
CodeBlockContent
The code surface as a standalone part. Compose it inside your own scroll
container and the block renders no internal scroller; omit it and the block
scrolls itself exactly as before. Takes only className.
CodeBlockCopyButton
Prop
Type
Default
Description
value
string
root code
Text to copy. Notation comments are stripped.
onCopy
(value: string) => void
-
Fired after a successful copy.
timeout
number
2000
How long the copied state lasts. 0 keeps it.
position
"auto" | "pinned" | "inline"
"auto"
auto is inline in a header, pinned elsewhere.
onCopyError
(error: unknown) => void
-
Fired when the clipboard write rejects.
labels
{ copy?, copied? }
English
Accessible names, for localisation.
alwaysVisible
boolean
false
Skips the hover reveal when pinned.
CodeBlockDownloadButton
Saves the block's code as a file - the sibling of the copy button for
builder-style output. Same position contract; filename defaults to
code.<ext> from the block's language.
Prop
Type
Default
Description
value
string
block code
Text to save, notation stripped.
filename
string
derived
Name for the saved file.
onDownload
(filename: string) => void
-
Fired after the save starts.
position
"auto" | "pinned" | "inline"
"auto"
Same placement contract as copy.
label
string
English
Accessible name, for localisation.
CodeBlockLineActions
Prop
Default
Type
Description
children
-
({ line, text, state }) => ReactNode
Rendered on the active line.
side
"end"
"end" | "gutter"
end floats the group over the end of the row; gutter centres it on the row's start edge, half over the numbers, without reserving any space. A fold toggle owns that edge, so a gutter action on a fold-start row falls back to end.
CodeBlockExpandButton
Renders nothing when the content is shorter than maxLines, so it is always
safe to compose.
useCodeBlockConfig
useCodeBlockConfig(partName) - the string only names your component in the
"must be used within a CodeBlock" error. Returns the block's own config for
anything composed inside it: code, language, resolvedLanguage,
showLineNumbers, wrap / setWrap, expanded / setExpanded,
collapsible, streaming and contentId.
useCodeBlockFolding
Live folding for anything composed inside the block: regions,
foldedStarts, toggleFold, foldAll, unfoldAll and foldable. Numbers
are source-based like every line spec.
useCodeBlockSelection
Live selection for anything composed inside the block - a line action or a
header control. Returns selectedLines (sorted), toggleLine,
clearSelection and selectable, so an "add selection to chat" affordance
can act on the whole range instead of only the hovered line.
Helpers
Export
From
Signature
Description
highlightCode
code-block-highlight
(code, options) => Promise<CodeBlockLine[]>
Isomorphic. Safe in a server component.
markdownFences
code-block
(markdown) => CodeBlockMarkdownPart[]
Splits prose from fenced code; flags an unclosed fence.
markdownCodeProps
code-block
(preProps) => { code, language }
Reads a react-markdown pre.
parseLineSpec
code-block-highlight
(spec) => Set<number>
"2-4,7" to line numbers.
stripNotationComments
code-block-highlight
(code) => string
Removes [!code ...] comments.
ansiToLines
code-block
(text, startLine?) => CodeBlockLine[]
Terminal SGR colours to renderable lines.
parseUnifiedDiff
code-block
(patch) => CodeBlockPatchFile[]
A git patch to per-file lines with dual gutters.
Data attributes
State is published as attributes, so styling never needs !important and never
needs a :has() selector.
Attribute
On
Values
data-variant
root
default, ghost
data-has-header
root
present when a header is composed
data-streaming
root
present while streaming
data-has-diff
root
present when any line carries diff state
data-foldable
root
present when folding is enabled
data-gutter-channel
root
present when folding reserves the gutter channel
data-code-line
line
the displayed line number
data-highlighted
line
present when marked
data-diff
line
add, remove
data-focused / data-blurred
line
focus mode
data-level
line
error, warning, info
data-selected
line
present when selected
data-active
line
present under the keyboard/hover cursor
data-gutter
line
a verbatim gutter label (patch old/new pairs)
data-code-line-numbers
pre
present when the gutter renders
data-wrap
pre
present while soft-wrapping
data-selectable
pre
present when line selection is on
data-word
token
the matched word on a marked token
data-position
copy / download
pinned, inline
data-copied
copy button
present during the copied beat
data-copy-failed
copy button
present for 2s after a rejected write
data-state
fold toggle
folded, unfolded
data-state
wrap toggle
on, off
data-state
expand
expanded, collapsed
data-side
line actions
end, gutter
Keyboard
Key
Action
Tab
Moves into the scroll region, which is a tab stop.
↑↓
Moves the active line when selectable.
Shift + ↑↓
Extends the selection.
EnterSpace
Toggles the active line's selection in place.
HomeEnd
Moves the active line to the first or last rendered line.
Esc
Clears the active line, when selectable.
EnterSpace
Folds or unfolds the region when a fold toggle has focus.
Accessibility
With the built-in surface, the scroll container is a labelled region and
a tab stop, so it can be scrolled without a pointer. Composed, the keyboard
scroll stop is your own container, which then needs its own accessible name.
With selectable, the code is a listbox and each line an option carrying
aria-selected. The region wrapper holds the only tabIndex={0}, so the block is a single tab stop; with selectable, arrow keys move the active option and aria-activedescendant on that wrapper exposes it to assistive tech.
Line numbers and diff glyphs are generated content, so they are outside the
accessibility tree and outside text selection: copying gives exact source.
Streaming sets aria-busy and announces completion once through a polite
status. The code is deliberately not a live region, which would otherwise
narrate every token.