Code Block
alphav0.2.1Code surface with view, edit, and terminal modes — Shiki highlighting, dual-theme CSS variables, and chrome presets for docs, chat, and terminal UIs.
Context
Substrate for every 'render code professionally' surface in the library — chat assistants, fenced markdown blocks, JSON / config viewers, card-tree code sections, virtual terminal walkthroughs, and snippet editors. View mode uses Shiki's GitHub Light + GitHub Dark Default themes (toggled via the active `.dark` class with zero re-tokenize). Edit mode wraps a CodeMirror 6 instance with a custom HighlightStyle approximating the same GitHub palette (a near-match, not pixel-perfect — a true Shiki → CodeMirror theme bridge is not implemented). Terminal mode renders structured `TerminalLine[]` rows with prompt detection on `$ `, `> `, `# ` prefixes and macOS-style traffic-light decoration. Streaming-friendly via an explicit `streaming` flag that batches re-tokenization to rAF and shows a blinking tail cursor. Filename → lang derivation works out of the box for ~30 extensions; consumer can override via `filenameToLang`. Object-shape callbacks throughout (per F-cross-12).
Installation
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add @ilinxa/code-blockAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/code-block-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Preview
1. View mode — TypeScript (the default)
Filename + language pill + copy button. GitHub Light / Dark token palette.
import { useState } from "react";import { Button } from "@/components/ui/button"; export default function Counter() { const [n, setN] = useState(0); return ( <div className="flex items-center gap-3"> <Button onClick={() => setN(n - 1)}>−</Button> <span className="font-mono">{n}</span> <Button onClick={() => setN(n + 1)}>+</Button> </div> );} 2. Line highlights + annotations
Highlighted rows draw the eye; severity icons in the gutter open tooltips with messages.
import { useState } from "react";import { Button } from "@/components/ui/button"; export default function Counter() { const [n, setN] = useState(0); return ( <div className="flex items-center gap-3"> <Button onClick={() => setN(n - 1)}>−</Button> <span className="font-mono">{n}</span> <Button onClick={() => setN(n + 1)}>+</Button> </div> );} 3. Streaming — chat assistant style
Click Replay to emit 10-char chunks at 50 ms intervals. Tail cursor blinks while streaming; tokenization stays smooth.
4. JSON config + long-block collapse
Long blocks fade out with a 'Show all' button. Click to expand inline.
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "id": 1, "geometry": { "type": "Point", "coordinates": [-42.4923, -16.8769] } }, { "type": "Feature", "id": 2, "geometry": { "type": "Point", "coordinates": [95.0154, 56.2462] } }, { "type": "Feature", "id": 3, "geometry": { "type": "Point", "coordinates": [-127.4769, -50.6307] } }, { "type": "Feature", "id": 4, "geometry": { "type": "Point", "coordinates": [10.0308, 22.4924] } }, { "type": "Feature", "id": 5, "geometry": { "type": "Point", "coordinates": [147.5385, -84.3845] } }, { "type": "Feature", "id": 6, "geometry": { "type": "Point", "coordinates": [-74.9538, -11.2614] } }, { "type": "Feature", "id": 7, "geometry": { "type": "Point", "coordinates": [62.5539, 61.8617] } }, { "type": "Feature", "id": 8, "geometry": { "type": "Point", "coordinates": [-159.9384, -45.0152] } }, { "type": "Feature", "id": 9, "geometry": { "type": "Point", "coordinates": [-22.4307, 28.1079] } }, { "type": "Feature", "id": 10, "geometry": { "type": "Point", "coordinates": [115.0770, -78.7690] } }, { "type": "Feature", "id": 11, "geometry": { "type": "Point", "coordinates": [-107.4153, -5.6459] } }, { "type": "Feature", "id": 12, "geometry": { "type": "Point", "coordinates": [30.0924, 67.4772] } }, { "type": "Feature", "id": 13, "geometry": { "type": "Point", "coordinates": [167.6001, -39.3997] } }, { "type": "Feature", "id": 14, "geometry": { "type": "Point", "coordinates": [-54.8922, 33.7234] } }, { "type": "Feature", "id": 15, "geometry": { "type": "Point", "coordinates": [82.6155, -73.1535] } }, { "type": "Feature", "id": 16, "geometry": { "type": "Point", "coordinates": [-139.8768, -0.0304] } }, { "type": "Feature", "id": 17, "geometry": { "type": "Point", "coordinates": [-2.3691, 73.0927] } }, { "type": "Feature", "id": 18, "geometry": { "type": "Point", "coordinates": [135.1386, -33.7842] } }, { "type": "Feature", "id": 19, "geometry": { "type": "Point", "coordinates": [-87.3537, 39.3389] } }, { "type": "Feature", "id": 20, "geometry": { "type": "Point", "coordinates": [50.1540, -67.5380] } }, { "type": "Feature", "id": 21, "geometry": { "type": "Point", "coordinates": [-172.3383, 5.5851] } }, { "type": "Feature", "id": 22, "geometry": { "type": "Point", "coordinates": [-34.8306, 78.7082] } }, { "type": "Feature", "id": 23, "geometry": { "type": "Point", "coordinates": [102.6771, -28.1687] } }, { "type": "Feature", "id": 24, "geometry": { "type": "Point", "coordinates": [-119.8152, 44.9544] } }, { "type": "Feature", "id": 25, "geometry": { "type": "Point", "coordinates": [17.6925, -61.9225] } }, { "type": "Feature", "id": 26, "geometry": { "type": "Point", "coordinates": [155.2002, 11.2006] } }, { "type": "Feature", "id": 27, "geometry": { "type": "Point", "coordinates": [-67.2921, 84.3237] } }, { "type": "Feature", "id": 28, "geometry": { "type": "Point", "coordinates": [70.2156, -22.5532] } }, { "type": "Feature", "id": 29, "geometry": { "type": "Point", "coordinates": [-152.2767, 50.5699] } }, { "type": "Feature", "id": 30, "geometry": { "type": "Point", "coordinates": [-14.7690, -56.3070] } }, { "type": "Feature", "id": 31, "geometry": { "type": "Point", "coordinates": [122.7387, 16.8161] } }, { "type": "Feature", "id": 32, "geometry": { "type": "Point", "coordinates": [-99.7536, 89.9392] } }, { "type": "Feature", "id": 33, "geometry": { "type": "Point", "coordinates": [37.7541, -16.9377] } }, { "type": "Feature", "id": 34, "geometry": { "type": "Point", "coordinates": [175.2618, 56.1854] } }, { "type": "Feature", "id": 35, "geometry": { "type": "Point", "coordinates": [-47.2305, -50.6915] } }, { "type": "Feature", "id": 36, "geometry": { "type": "Point", "coordinates": [90.2772, 22.4316] } }, { "type": "Feature", "id": 37, "geometry": { "type": "Point", "coordinates": [-132.2151, -84.4453] } }, { "type": "Feature", "id": 38, "geometry": { "type": "Point", "coordinates": [5.2926, -11.3222] } }, { "type": "Feature", "id": 39, "geometry": { "type": "Point", "coordinates": [142.8003, 61.8009] } }, { "type": "Feature", "id": 40, "geometry": { "type": "Point", "coordinates": [-79.6920, -45.0760] } }, { "type": "Feature", "id": 41, "geometry": { "type": "Point", "coordinates": [57.8157, 28.0471] } }, { "type": "Feature", "id": 42, "geometry": { "type": "Point", "coordinates": [-164.6766, -78.8298] } }, { "type": "Feature", "id": 43, "geometry": { "type": "Point", "coordinates": [-27.1689, -5.7067] } }, { "type": "Feature", "id": 44, "geometry": { "type": "Point", "coordinates": [110.3388, 67.4164] } }, { "type": "Feature", "id": 45, "geometry": { "type": "Point", "coordinates": [-112.1535, -39.4605] } }, { "type": "Feature", "id": 46, "geometry": { "type": "Point", "coordinates": [25.3542, 33.6626] } }, { "type": "Feature", "id": 47, "geometry": { "type": "Point", "coordinates": [162.8619, -73.2143] } }, { "type": "Feature", "id": 48, "geometry": { "type": "Point", "coordinates": [-59.6304, -0.0912] } }, { "type": "Feature", "id": 49, "geometry": { "type": "Point", "coordinates": [77.8773, 73.0319] } }, { "type": "Feature", "id": 50, "geometry": { "type": "Point", "coordinates": [-144.6150, -33.8450] } }, { "type": "Feature", "id": 51, "geometry": { "type": "Point", "coordinates": [-7.1073, 39.2781] } }, { "type": "Feature", "id": 52, "geometry": { "type": "Point", "coordinates": [130.4004, -67.5988] } }, { "type": "Feature", "id": 53, "geometry": { "type": "Point", "coordinates": [-92.0919, 5.5243] } }, { "type": "Feature", "id": 54, "geometry": { "type": "Point", "coordinates": [45.4158, 78.6474] } }, { "type": "Feature", "id": 55, "geometry": { "type": "Point", "coordinates": [-177.0765, -28.2295] } }, { "type": "Feature", "id": 56, "geometry": { "type": "Point", "coordinates": [-39.5688, 44.8936] } }, { "type": "Feature", "id": 57, "geometry": { "type": "Point", "coordinates": [97.9389, -61.9833] } }, { "type": "Feature", "id": 58, "geometry": { "type": "Point", "coordinates": [-124.5534, 11.1398] } }, { "type": "Feature", "id": 59, "geometry": { "type": "Point", "coordinates": [12.9543, 84.2629] } }, { "type": "Feature", "id": 60, "geometry": { "type": "Point", "coordinates": [150.4620, -22.6140] } } ]} 5. Terminal — virtual install walkthrough
Structured `lines: TerminalLine[]` API. Input rows show the prompt; output rows are muted; error rows in destructive red. macOS traffic-light decoration optional.
6. Edit mode — controlled CodeMirror editor
Same JetBrains Mono font, same line-height, near-match token colors via custom CodeMirror HighlightStyle. v0.2.0 will swap in a pixel-perfect Shiki bridge.
7. Custom header — action slot
The header `actions` slot accepts any ReactNode; pre-built buttons (copy, wrap, expand) compose around it.
// click Run to evaluate (consumer-managed)console.log("hello from ilinxa");8. Python sample — multi-language coverage
Shiki loads grammars on demand; common languages stay synchronously bundled (~10), the rest dynamic-import.
from dataclasses import dataclass @dataclassclass User: id: int name: str email: str | None = None def greet(user: User) -> str: if user.email: return f"Hello {user.name} <{user.email}>" return f"Hello {user.name}" users = [ User(id=1, name="Ada", email="ada@example.com"), User(id=2, name="Linus"),] for u in users: print(greet(u)) 9. Error trace — annotations on a plain log
`lang='plaintext'` works as a viewer for arbitrary text. Combine with annotations + line-highlights for a debug surface.
TypeError: Cannot read property 'name' of undefined at processUser (/app/src/users.ts:7:24) at /app/src/index.ts:15:3 at Array.map (<anonymous>) at greetAll (/app/src/index.ts:14:18) at Object.<anonymous> (/app/src/index.ts:42:1) at Module._compile (node:internal/modules/cjs/loader.js:1234:30) 10. Inline JSON (no chrome)
`header={false}` for a minimal embed.
{ "name": "ilinxa-ui-pro", "version": "0.1.0", "private": true, "dependencies": { "next": "^16.2.0", "react": "^19.2.0", "shiki": "^4.0.2", "@codemirror/state": "^6.6.0" }, "scripts": { "dev": "next dev", "build": "next build", "lint": "eslint" }} Demo source
"use client";import { useEffect, useRef, useState } from "react";import { Button } from "@/components/ui/button";import { CodeBlock } from "./code-block";import { SAMPLE_EDIT_DEFAULT, SAMPLE_ERROR_ANNOTATIONS, SAMPLE_ERROR_TRACE, SAMPLE_JSON, SAMPLE_LONG_JSON, SAMPLE_PYTHON, SAMPLE_TERMINAL, SAMPLE_TS, SAMPLE_TS_HIGHLIGHTED, chunkString,} from "./dummy-data"; function Section({ title, caption, children,}: { title: string; caption?: string; children: React.ReactNode;}) { return ( <section className="flex flex-col gap-2"> <div className="flex flex-col gap-1"> <h3 className="text-sm font-medium text-foreground">{title}</h3> {caption ? ( <p className="text-xs text-muted-foreground">{caption}</p> ) : null} </div> {children} </section> );} function StreamingDemo() { const [value, setValue] = useState(""); const [streaming, setStreaming] = useState(false); const timeoutsRef = useRef<number[]>([]); const replay = () => { // Cancel anything in flight for (const id of timeoutsRef.current) window.clearTimeout(id); timeoutsRef.current = []; setValue(""); setStreaming(true); const chunks = chunkString(SAMPLE_TS, 10); let acc = ""; chunks.forEach((c, i) => { const id = window.setTimeout(() => { acc += c; setValue(acc); if (i === chunks.length - 1) { // Final tokenize on stop window.setTimeout(() => setStreaming(false), 80); } }, i * 50); timeoutsRef.current.push(id); }); }; useEffect(() => { return () => { for (const id of timeoutsRef.current) window.clearTimeout(id); }; }, []); return ( <div className="flex flex-col gap-2"> <div className="flex items-center gap-2"> <Button size="sm" variant="outline" onClick={replay}> Replay streaming </Button> <span className="text-xs text-muted-foreground"> {streaming ? "Streaming…" : value ? "Done" : "Click to start"} </span> </div> <CodeBlock lang="tsx" filename="counter.tsx" value={value} streaming={streaming} showCopy /> </div> );} function EditDemo() { const [code, setCode] = useState(SAMPLE_EDIT_DEFAULT); const [saved, setSaved] = useState<string | null>(null); return ( <div className="flex flex-col gap-2"> <CodeBlock mode="edit" lang="ts" filename="greet.ts" value={code} onChange={({ value }) => setCode(value)} onSave={({ value }) => setSaved(value)} showCopy /> <div className="flex items-center gap-3 text-xs text-muted-foreground"> <span>Press Cmd/Ctrl+S to save.</span> {saved ? ( <span className="text-foreground"> ✓ Saved {saved.length} chars </span> ) : null} </div> </div> );} export default function CodeBlockDemo() { return ( <div className="flex flex-col gap-10"> <Section title="1. View mode — TypeScript (the default)" caption="Filename + language pill + copy button. GitHub Light / Dark token palette." > <CodeBlock lang="tsx" filename="counter.tsx" value={SAMPLE_TS} /> </Section> <Section title="2. Line highlights + annotations" caption="Highlighted rows draw the eye; severity icons in the gutter open tooltips with messages." > <CodeBlock lang="tsx" filename="counter.tsx" value={SAMPLE_TS} highlightedLines={SAMPLE_TS_HIGHLIGHTED} annotations={[ { line: 5, type: "info", message: "Initial state is 0" }, { line: 9, type: "warn", message: "Consider a setter callback" }, ]} showLineNumbers /> </Section> <Section title="3. Streaming — chat assistant style" caption="Click Replay to emit 10-char chunks at 50 ms intervals. Tail cursor blinks while streaming; tokenization stays smooth." > <StreamingDemo /> </Section> <Section title="4. JSON config + long-block collapse" caption="Long blocks fade out with a 'Show all' button. Click to expand inline." > <CodeBlock lang="json" filename="package.json" value={SAMPLE_LONG_JSON} showLineNumbers maxLines={12} /> </Section> <Section title="5. Terminal — virtual install walkthrough" caption="Structured `lines: TerminalLine[]` API. Input rows show the prompt; output rows are muted; error rows in destructive red. macOS traffic-light decoration optional." > <CodeBlock mode="terminal" filename="zsh" showTrafficLights lines={SAMPLE_TERMINAL} /> </Section> <Section title="6. Edit mode — controlled CodeMirror editor" caption="Same JetBrains Mono font, same line-height, near-match token colors via custom CodeMirror HighlightStyle. v0.2.0 will swap in a pixel-perfect Shiki bridge." > <EditDemo /> </Section> <Section title="7. Custom header — action slot" caption="The header `actions` slot accepts any ReactNode; pre-built buttons (copy, wrap, expand) compose around it." > <CodeBlock lang="ts" filename="run-me.ts" value={`// click Run to evaluate (consumer-managed)\nconsole.log("hello from ilinxa");`} showCopy showWrap actions={ <Button size="sm" variant="outline" className="h-7"> ▶ Run </Button> } /> </Section> <Section title="8. Python sample — multi-language coverage" caption="Shiki loads grammars on demand; common languages stay synchronously bundled (~10), the rest dynamic-import." > <CodeBlock lang="python" filename="users.py" value={SAMPLE_PYTHON} showLineNumbers showWrap showExpand /> </Section> <Section title="9. Error trace — annotations on a plain log" caption="`lang='plaintext'` works as a viewer for arbitrary text. Combine with annotations + line-highlights for a debug surface." > <CodeBlock lang="plaintext" filename="stderr.log" value={SAMPLE_ERROR_TRACE} highlightedLines={[1]} annotations={SAMPLE_ERROR_ANNOTATIONS} showLineNumbers /> </Section> <Section title="10. Inline JSON (no chrome)" caption="`header={false}` for a minimal embed." > <CodeBlock lang="json" value={SAMPLE_JSON} header={false} /> </Section> </div> );} Usage
When to use
CodeBlockis the substrate for every “render code professionally” surface in the library: chat assistants, fenced markdown blocks, JSON/config viewers, card-tree “code” sections, virtual terminal walkthroughs, and snippet editors. Three modes (view / edit / terminal) cover the common cases. Streaming-friendly. Language-agnostic.
Basic — view
import { CodeBlock } from "@/components/code-block";
<CodeBlock filename="app.tsx" value={code} />Edit mode (controlled)
<CodeBlock
mode="edit"
lang="ts"
filename="greet.ts"
value={code}
onChange={({ value }) => setCode(value)}
onSave={({ value }) => save(value)}
/>Streaming (chat assistant)
<CodeBlock
lang="ts"
value={partial}
streaming={isStillStreaming}
/>Terminal
<CodeBlock
mode="terminal"
showTrafficLights
lines={[
{ kind: "input", text: "$ pnpm install" },
{ kind: "output", text: "Resolving... done" },
{ kind: "error", text: "ENOENT: no such file" },
]}
/>Content-Security-Policy (read this before self-hosting)
The default highlighter engine is WebAssembly. Under a strict CSP your script-src must include 'wasm-unsafe-eval', or the browser refuses the compile outright (Wasm code generation disallowed by embedder) and highlighting can never run — no component can override its host's CSP. Either widen the policy, or switch engines:
// No WebAssembly at all — for CSP-restricted hosts.
// Slightly narrower grammar coverage is the trade.
<CodeBlock value={code} lang="ts" regexEngine="javascript" />Highlighters are cached per engine, so mixing both on one page is safe. Either way the block never goes blank: if highlighting is unavailable the raw code renders as plain text and the body is marked data-highlight="failed".
Notes
- Filename → lang priority:
lang(if set) wins; elsefilenameToLangconsumer override; else built-in extension map; elseplaintext. - Streaming: explicit
streamingprop. Final clean-tokenize fires when you flip it back tofalse. Don't auto-detect from update frequency. - RSC posture: the v0.1.0 client variant SSRs the first paint fine in Next.js. A dedicated zero-client-Shiki
/serverexport defers to a later minor. - Edit ↔ view continuity: near-match in v0.1.0 (custom CodeMirror HighlightStyle approximating GitHub Light + Dark Default). Pixel-perfect Shiki bridge defers to a later minor.
- Diff: use
lang="diff"for unified diff text in v0.1.0; split-view will be a siblingcode-diffcomponent. - Soft-failure: a failed editor mount renders an inline error plus a Reload as view-only control (labels
editorFailed/reloadAsViewOnly); a failed highlight renders the raw code. Neither ever leaves an empty panel. - Long blocks: opt-in via
maxLines; renders a fade-out + “Show all (N more lines)” button.
Features
- Three render modes (view / edit / terminal) in one component, switched by `mode` prop
- Shiki tokenization for view mode (GitHub Light + Dark Default by default; consumer overridable)
- CodeMirror 6 in edit mode with custom HighlightStyle for near-match view/edit visual continuity
- Streaming-friendly: explicit `streaming` prop, rAF-batched re-tokenization, blinking tail cursor
- Terminal mode with `lines: TerminalLine[]` API (input / output / error kinds) + prompt detection + optional macOS traffic-lights
- Chrome: filename pill, language label, copy button (with success animation), expand-to-modal, wrap toggle, download button — all gated by `show*` flags
- Body: optional line numbers (default off in view), wrap or scroll, highlighted line ranges, severity-icon annotations with tooltips, long-block collapse with 'Show all'
- Filename → lang priority chain: `lang` prop > consumer `filenameToLang` > built-in 30-entry extension map > plaintext
- Dual-theme via CSS variables: `.dark` class toggles palette with zero re-tokenize
- Standalone header parts exported (`<CodeBlockCopyButton>`, `<CodeBlockTrafficLights>`, etc.) for `renderHeader` slot composition
- Object-shape callbacks throughout (per F-cross-12)
- Imperative handle: `copy()`, `focus()`, `getValue()`, and `scrollToLine()` — which works in all three modes (CodeMirror dispatch in edit, row targeting in view/terminal) and expands a collapsed block rather than scrolling to a row hidden under the fade