Code block
StableHighlighted code in five grey roles, with line numbers, picked-out lines, a Wrap toggle, Copy, and long files folded.
The brand is black and white, so the highlighter tells token kinds apart by weight, tone and a faint tint instead of colour. Line numbers are never copied or read aloud, the Copy button keeps its name, and files over 24 lines fold to 20 with a “Show all” button. Snippet is a one-line command; InlineCode sits inside a sentence.
A file with picked-out lines
import { CodeBlock } from '@rchursin/product/ui'
const source = `import { useState } from 'react'import { Button } from '@rchursin/product/ui'
/** Saves, and keeps focus on the button while it works */export function SaveButton({ onSave }: { onSave: () => Promise<void> }) { const [saving, setSaving] = useState(false)
async function save() { setSaving(true) try { await onSave() } finally { setSaving(false) } }
return ( <Button loading={saving} onClick={save}> {saving ? 'Saving…' : 'Save'} </Button> )}`
/** A file with a title, line numbers and three lines picked out. Highlight only the lines you talk about. */export default function CodeBlockTsx() { return <CodeBlock code={source} lang="tsx" title="SaveButton.tsx" highlightLines={[18, 19, 20]} className="w-full max-w-2xl" />}Usage
import { CodeBlock, InlineCode, Snippet } from '@rchursin/product/ui'<CodeBlock code={source} lang="tsx" title="button.tsx" highlightLines={[4, 5]} /><Snippet command="npm install" />Examples
Commands
Snippet for one command: the $ is drawn but never copied. Shell blocks leave out line numbers.
import { CodeBlock, Snippet } from '@rchursin/product/ui'
const steps = `# Start the dev servernpm run dev -- --port 5174
# Check types before you pushnpm run typecheck`
/** Snippet is one command with a $ prompt that is never copied. Short shell blocks drop the line numbers. */export default function CodeBlockSnippet() { return ( <div className="flex w-full max-w-lg flex-col gap-4"> <Snippet command="npm install" label="install command" /> <CodeBlock code={steps} lang="bash" title="Terminal" /> </div> )}A long file, folded
Past 24 lines it shows 20, fades the last few and offers Show all. These are the highlighting rules themselves.
import { CodeBlock } from '@rchursin/product/ui'
/* The highlighting rules themselves (from styles/product.css): five roles, greys only */const css = `/* Code: monochrome highlighting, five roles */.rc-code { --rc-code-tint: color-mix(in srgb, var(--rc-fg) 6.5%, transparent); color: var(--rc-fg); font-variant-ligatures: none; font-synthesis: none; tab-size: 2;}
/* Muted: punctuation, operators, comments, line numbers */.rc-code .t-punct,.rc-code .t-comment,.rc-code .rc-ln { color: var(--rc-fg-3);}
/* Literal: strings and numbers, base text on a faint tint */Inline code
Mono at 0.86 em on a faint tint, so it sits on the line without making it taller.
Wrap the app in ToastProvider once, then call useToast() wherever something finishes. Pass an action to offer Undo instead of asking “Are you sure?” first.
import { InlineCode } from '@rchursin/product/ui'
/** Code inside a sentence: mono at 0.86 em on a faint tint, so it sits on the line without raising it. */export default function CodeBlockInline() { return ( <p className="max-w-md text-[15px]/7 text-pretty text-fg-2"> Wrap the app in <InlineCode>ToastProvider</InlineCode> once, then call <InlineCode>useToast()</InlineCode> wherever something finishes. Pass an{' '} <InlineCode>action</InlineCode> to offer Undo instead of asking “Are you sure?” first. </p> )}API
CodeBlock props
| Prop | Type | Default | Description |
|---|---|---|---|
coderequired | string | – | The source text. Copy copies exactly this. |
lang | 'tsx' | 'ts' | 'css' | 'html' | 'bash' | 'json' | 'text' | 'tsx' | How to highlight it. text turns highlighting off. langOf(fileName) guesses it from a file name. |
title | ReactNode | – | File name or caption in the header. A string title is also the accessible name and the Copy button’s name (“Copy button.tsx”). |
label | string | 'Code' | Accessible name when the title isn’t a string. |
lineNumbers | boolean | more than 3 lines, not bash | Show line numbers. They are aria-hidden and can’t be selected. |
highlightLines | number[] | – | 1-based lines to pick out. Highlight only the lines the text around it talks about. |
collapseAfter | number | 24 | Fold files longer than this to 20 lines with a Show all button. 0 turns folding off. |
header | boolean | true | Show the header row (title, Wrap, Copy). |
actions | ReactNode | – | Extra controls in the header, before Wrap and Copy. |
bare | boolean | false | Drop the outer card and the header rule, for code inside another card or a tab panel. |
className | string | – | Extra classes on the <figure>. |
Snippet props
| Prop | Type | Default | Description |
|---|---|---|---|
commandrequired | string | – | One command, without the $. Cut with an ellipsis if it doesn’t fit; Copy still copies all of it. |
label | string | 'command' | What the Copy button copies, in its name: “Copy install command”. |
className | string | – | Extra classes on the row. |
InlineCode props
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The code, rendered in a <code> element. |
className | string | – | Extra classes. |
CSS
| Class or variable | What it does |
|---|---|
rc-code | Turns on the five grey roles for everything inside. Base: keywords, names, calls and JSX in the text colour. |
rc-code .t-punct, .t-comment, .rc-ln | Muted: punctuation, operators, comments and line numbers in fg-3 (4.9:1 light, 7.3:1 dark, on the sunken block). Comments differ by tone alone, since Geist Mono has no italic. |
rc-code .t-string, .t-number | Literal: strings and numbers keep the base colour on a faint 6.5% tint. |
rc-code .t-definition | Definition: a name being declared (after const, function, type …) in weight 600. |
rc-code .rc-line-on | Emphasis: a highlighted line gets a 4% band and a 2px bar in the base colour (a solid border in forced-colors mode). |
rc-code-fade | Fades the last 4.5rem of a folded file into the ground with a mask. |
rc-code[data-wrap] | Wrap mode: long lines wrap anywhere with a 2ch hanging indent. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Wrap (from 640 pixels wide), then Copy, then the code itself when it is wider than its box, then Show all. |
| EnterSpace | Presses Wrap, Copy or Show all. |
| Left arrowRight arrow | Scrolls the code sideways once it has focus. |
- The block is a <figure> with the header as its <figcaption>; the code is a <pre><code>.
- The code only becomes a Tab stop (tabIndex 0, role="region", named after the title) while it actually overflows, re-checked on resize, so short blocks don’t add stops.
- Copy keeps its name (“Copy SaveButton.tsx”); the result shows as a check for 2 seconds and is announced (“Copied SaveButton.tsx”). Where the clipboard is blocked it falls back, and says to press Ctrl C or ⌘ C if that fails too.
- Wrap is a toggle button (aria-pressed). The choice is remembered for this visitor and applies to every code block on the site.
- Show all is a disclosure: aria-expanded, and aria-controls points at the code.
- Line numbers and the Snippet’s $ are aria-hidden and can’t be selected, so they are never read or copied.
Guidelines
Do
- Give files a title: it names the region and the Copy button.
- Highlight only the lines the text around the block discusses.
- Use Snippet for a single command someone will paste.
Don’t
- Don’t turn folding off for long files on a page people scan.
- Don’t put a whole file in InlineCode; it doesn’t scroll or wrap safely.
- Don’t add colour to the roles; the greys are tuned for contrast.
In use
Used on this site
Related
- highlightA small, dependency-free syntax highlighter for TSX, TS, CSS, HTML, shell and JSON, drawn in greys, weight and tint instead of colour.
- Copy buttonCopy text to the clipboard with one press: a pearl button that says “Copied”, a quiet one for code, and the helper under both.
- Command lineA one-line shell command with a $ prompt and a Copy command button.
- TerminalA dark terminal window with sample scan output: usage bars, a context cost and a verdict per item, printed line by line.
Source
The component’s own files, exactly as they run on this site.
/** * Code, shown well (research: Geist CodeBlock, Primer, Roselli on scrolling regions, Tonsky on * highlighting): monochrome roles instead of colours, line numbers that are never copied or read * aloud, highlighted lines, a Wrap toggle, a Copy button whose name never changes, and long files * folded with a "Show all" toggle. * * <CodeBlock code={source} lang="tsx" title="button.tsx" /> * <CodeBlock code="npm run dev" lang="bash" /> // short: no numbers * <CodeBlock code={src} highlightLines={[4, 5]} collapseAfter={0} /> * <InlineCode>useToast()</InlineCode> * * The scrolling area becomes focusable (tabIndex 0, role region, named) only while it actually * overflows, so keyboard users can scroll it without every short block adding a Tab stop. */import { useEffect, useId, useLayoutEffect, useMemo, useRef,