Display
Code Block
A self-contained, copyable code panel with its own regex syntax highlighter — line numbers, line highlighting, tabs and a wrap toggle, zero external highlighting engine.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/code-block.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "CodeBlock" component. Self-contained:
write your own minimal regex tokenizer, no Shiki/Prism/highlight.js, no
dangerouslySetInnerHTML.
Contract
- Export a forwardRef div. Props: code?: string, tabs?: { label: string,
code: string, language?: string }[] (tabs takes priority over code when
both are given), language = "tsx", filename?: string,
showLineNumbers = false, highlightLines?: number[] (1-based),
maxHeight?: number | string, wrap = false, copyable = true, caption?: string.
- Highlighter: a tokenizeLine(line, language) function (~60 lines) that cuts
one line into { text, type } tokens where type is comment | string |
number | keyword | function | plain, using ONE regex per language built
from named capture groups (comment, string, number, ident), executed in a
loop that always appends the untouched gap before each match as a "plain"
token. This guarantees every token.text concatenated back together
reproduces the input line exactly — self-test that invariant by hand.
Support tsx/ts/js/jsx (// comments, "/'/` strings, JS keyword set,
identifier-followed-by-"(" => function), json (no comments, "-strings
only, true/false/null as keywords) and bash/sh (# comments, "/' strings,
shell keyword set). Unknown languages fall back to one plain token —
document this as a real limitation, not a bug.
An identifier is classified in this order: keyword set FIRST, then the
"followed by (" call heuristic. Testing the paren first paints `if (`,
`for (`, `while (`, `switch (`, `catch (` and `return (` with the function
color — they are control flow, not calls.
Wrap the rendered lines in a real <pre><code class="language-<lang>">: it is
the semantic pair for source text, keeps line breaks on copy/paste, and lets
assistive tech announce the block as code. The per-line rows stay flex divs
inside it so the gutter and the highlight bar still work; give <pre> m-0 p-0
because a surrounding prose stylesheet would otherwise add its own margins.
Behavior
- Tabs (when given) render a role="tablist" strip; each tab is role="tab"
with aria-selected, roving tabIndex (0 on the active tab, -1 on the rest),
and ArrowLeft/ArrowRight/Home/End move focus AND selection. The single
content region below is role="tabpanel", aria-labelledby the active tab's
id. Switching tabs resets the active index via the adjust-state-during-
render pattern (compare a previous "tab labels joined" key during render,
not a setState-in-effect) whenever the tab set itself changes.
- Copy button copies the ACTIVE tab's raw `code` string (never text read
back out of the highlighted DOM). Try navigator.clipboard.writeText first;
on any failure (unsupported API, insecure context, permission denial) fall
back to a hidden-textarea + document.execCommand("copy"); if that also
fails, show a visible "Copy failed — select the code manually." message
(role="status") instead of failing silently. On success, swap the icon to
a checkmark for 2 seconds (cleared with clearTimeout on unmount and on a
fresh click), and mirror the state into an aria-live="polite" sr-only span.
- Wrap toggle button in the header always renders regardless of the `wrap`
prop; `wrap` only sets the initial state. Toggling flips every line
between whitespace-pre (default; overflows and the panel scrolls
horizontally) and whitespace-pre-wrap break-words (reflows, no horizontal
scroll).
- Line numbers render in a separate leading gutter span with select-none +
aria-hidden="true" so a user drag-selecting the code never picks up the
numbers, and screen readers never hear them.
- highlightLines marks matching 1-based line rows with a background tint
plus a 2px accent bar pinned to the left edge of the row (aria-hidden,
absolutely positioned) — both survive wrap and horizontal scroll.
- maxHeight (number | string) caps the content region and turns on
overflow-auto; when set (or inside a tabpanel) the region gets tabIndex=0
so keyboard users can focus it and scroll/read with arrow keys.
Rendering & styling
- Semantic tokens only: bg-card for the shell, bg-muted/40 for the header,
border for dividers, text-muted-foreground for the filename/gutter/caption,
bg-primary/10 + bg-primary for the highlighted-line tint and accent bar,
text-destructive + bg-destructive/10 for the copy-failure banner. Token
colors: comments -> text-muted-foreground, strings/numbers/keywords/
function-names -> var(--chart-1) through var(--chart-5) (one each),
everything else (punctuation, whitespace, plain identifiers) ->
text-foreground. cn() merges consumer className throughout.
- Hover/focus transitions on the header buttons are motion-reduce:transition
-none — there is no auto-playing animation here, so reduced motion only
needs to drop the CSS transition, never a behavior.
- Accessible: focus-visible rings on every button, aria-pressed on the wrap
toggle, aria-selected/aria-controls/role wiring on tabs, sr-only labels on
icon-only buttons.
Customization levers
- Token palette: remap the 6-entry TOKEN_CLASS table to different
var(--chart-N) slots (or add a 7th "attribute" category) without touching
the tokenizer.
- Add a language: one entry in the LANGUAGES config (keyword set + comment
style + whether backtick strings apply) — the tokenizer loop is unchanged.
- Density: shrink text-[13px]/leading-6 and the gutter's px-4 for a denser
panel, or drop showLineNumbers entirely for prose-embedded snippets.
- Header layout: swap the plain filename <span> for a language badge, or
hide the wrap button via a wrapper that only renders CodeBlock when a line
is known to be short.
- Highlight color: bg-primary/10 + bg-primary reads as "this changed"; swap
to bg-destructive/10 + bg-destructive for a "this is wrong" reading, or
var(--chart-2) tones for "this is new".Concepts
- Line-preserving tokenizer — the regex loop always emits the untouched gap between matches as a "plain" token, which is what guarantees the tokens reconstruct the exact input line (no silently dropped characters).
- Gutter selection safety — line numbers live in their own
select-nonespan ahead of the code span, so a mouse drag across a line copies only the code, andaria-hiddenkeeps them out of the accessibility tree entirely. - Copy the source, not the DOM — the copy button always copies the plain
codestring it was given, nevertextContentread back out of the highlighted spans, so copied output can't pick up rendering artifacts. - Honest clipboard fallback —
navigator.clipboardfailing (insecure context, permission denial, unsupported browser) falls through to a legacyexecCommandcopy, and only shows a visible failure message if that fails too — never a silent no-op. - Wrap is a real reflow, not a visual trick — toggling swaps
whitespace-preforwhitespace-pre-wrap break-wordson every line, trading horizontal scroll for reflowed lines. - Active-tab-scoped everything — highlighting, line numbers and copy all read from whichever tab is currently active; switching tabs resets to a fresh view without an effect-driven state cascade.
Animated List
A flex column that staggers items in on first mount and pops, exits and re-flows them as children are added or removed.
JSON Viewer
A collapsible, searchable JSON tree viewer with type-colored values, per-node copy value/path, and safe rendering of circular refs and every JS edge-case value.