highlight
StableA small, dependency-free syntax highlighter for TSX, TS, CSS, HTML, shell and JSON, drawn in greys, weight and tint instead of colour.
The lexer under every code block on this site. It understands enough TSX to keep JSX text, attributes and {expressions} apart, which a plain regex pass gets wrong, and returns plain data: tokens with a kind, split into lines. The same code runs in the prerender and the browser, so highlighted code is in the static HTML. Pair it with the rc-code classes: comments and punctuation turn grey, strings and numbers get a faint tint, declared names get weight, and every other kind stays in the base colour.
Tokens of a snippet
import { useId, useState } from 'react'import { tokenize, type Lang } from '@rchursin/product/lib/highlight'import { Segmented, TextInput } from '@rchursin/product/ui'
/** * tokenize() returns what the code block draws: every token with its kind. Edit the line and * watch the lexer keep JSX text, attributes and the expression inside {…} apart. */const snippets: Record<Exclude<Lang, 'html' | 'text'>, string> = { tsx: '<Button size="sm" onClick={() => save(draft)}>Save for later</Button>', ts: 'const total = runs.reduce((sum, r) => sum + r.cost, 0) // dollars', css: '.lux-card { border-radius: 16px; color: var(--rc-fg); }', bash: 'npm install --save-dev vite # dev only', json: '{ "name": "rchursin-ui", "private": true, "version": 3 }',}type Shown = keyof typeof snippets
export default function HighlightTokens() { const id = useId() const [lang, setLang] = useState<Shown>('tsx')Usage
import { highlight, tokenize, langOf, type Lang, type Token } from '@rchursin/product/lib/highlight'const lines = highlight(source, langOf('Button.tsx'))// [[{ kind: 'keyword', text: 'import' }, { kind: 'plain', text: ' ' }, …], …]Examples
Your own code view
langOf picks the language from the file name; highlight() gives lines; the t-* classes inside .rc-code do the greys.
import { useState } from 'react'import { highlight, langOf } from '@rchursin/product/lib/highlight'import { Segmented } from '@rchursin/product/ui'
/** * Your own code view in a few lines: langOf picks the language from the file name, highlight() * splits the tokens into lines, and the rc-code classes give each kind its grey (t-comment, * t-string, t-definition and so on). Line numbers are hidden from copy and from screen readers, * and long lines wrap instead of scrolling. */const files = { 'useSavedLabel.ts': `/* "Saved" one second after the last change */export function useSavedLabel(text: string) { const [saved, setSaved] = useState(true) useEffect(() => { setSaved(false) const t = setTimeout(() => setSaved(true), 1000) return () => clearTimeout(t) }, [text]) return saved ? 'Saved' : 'Saving…'API
highlight(code, lang)
| Prop | Type | Default | Description |
|---|---|---|---|
coderequired | string | – | Source text. Windows line endings are normalised and one trailing newline is dropped. |
langrequired | 'tsx' | 'ts' | 'css' | 'html' | 'bash' | 'json' | 'text' | – | text returns the code as one plain token. |
returns | Line[] | – | One array of tokens per line (Line = Token[]). A token that spans lines, like a block comment, is cut at each newline. Empty lines are empty arrays. |
tokenize(code, lang)
| Prop | Type | Default | Description |
|---|---|---|---|
code, langrequired | string, Lang | – | As above. |
returns | Token[] | – | The flat list of { kind, text }. In TSX, TS and HTML, neighbouring plain, text and punct tokens are merged into one. |
langOf(file) · TokenKind
| Prop | Type | Default | Description |
|---|---|---|---|
langOf(file) | (file: string) => Lang | – | By extension: tsx/jsx → tsx, ts/js/mjs → ts, css, html/svg → html, json, sh → bash, anything else → text. |
TokenKind | 'plain' | 'comment' | 'keyword' | 'string' | 'number' | 'type' | 'function' | 'property' | 'tag' | 'attr' | 'punct' | 'text' | 'variable' | 'definition' | – | definition is a name being declared (after const, function, type…); text is JSX text between tags. |
CSS
| Class or variable | What it does |
|---|---|
rc-code | On the wrapper: base text in the foreground colour, ligatures off, tab size 2. |
t-punct · t-comment | Muted (fg-3): punctuation, operators and comments. |
t-string · t-number | Literals: base text on a faint tint. |
t-definition | Names being declared, at weight 600. |
rc-ln | Line numbers in the muted grey. Add aria-hidden and select-none so they are never read or copied. |
Accessibility
- The styled kinds differ by tone, weight and tint, never by colour, and every tone keeps at least 4.5:1 against the code ground in both themes.
- The tokens are plain text in spans, so screen readers read the code as written.
Guidelines
Do
- Hide line numbers from screen readers and from selection.
- Let long lines wrap or scroll inside the code area, never the page.
Don’t
- Don’t use it for languages it doesn’t know; pass "text" instead of a near miss.
- Don’t add colours per kind; the brand is black and white.
In use
Used on this site
Related
- Code blockHighlighted code in five grey roles, with line numbers, picked-out lines, a Wrap toggle, Copy, and long files folded.
- 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.
- cxJoins class names and skips anything falsy, so each condition sits next to the classes it adds.
Source
The component’s own files, exactly as they run on this site.
/** * A small, dependency-free syntax highlighter for the docs (TSX, TS, CSS, HTML, shell, JSON). * * The brand is black and white, so token kinds are told apart by weight, style and grey level * instead of colour (see .rc-code in site.css). The lexer understands enough TSX to keep JSX * text, attributes and embedded expressions apart, which a plain regex pass gets wrong: * * <Button size="sm" onClick={() => save(for)}>Save for later</Button> * ^attr ^string ^expression ^text, not keywords * * Output is a list of lines, each a list of tokens, so line numbers and highlighted lines are * just markup. Runs the same on the server (prerender) and in the browser. */
export type Lang = "tsx" | "ts" | "css" | "html" | "bash" | "json" | "text"
export type TokenKind = | "plain" | "comment" | "keyword"