Skip to content

Code Block

Compound code display with three visual variants (hairline, bare, terminal), per-line diff/highlight states, an optional copy button, and syntax highlighting through a caller-provided lowlight instance. Renders as a <figure> with accessible name resolution via aria-labelledby (CodeBlock.Label) or aria-label fallback.

Preview

Installation

$pnpm exec dgadd add ui/code-block
[Installs to]src/components/ui/code-block[Item]ui/code-block

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Usage

tsx
import { CodeBlock } from "@/components/ui/code-block";const code = `import { Button } from "@/components/ui/button"export function App() {  return (    <Button variant="primary">      Click me    </Button>  )}`;export default function CodeBlockDefault() {  return (    <CodeBlock language="tsx">      <CodeBlock.Header>        <CodeBlock.Label>app.tsx</CodeBlock.Label>        <CodeBlock.CopyButton source={code} />      </CodeBlock.Header>      <CodeBlock.Content>{code}</CodeBlock.Content>    </CodeBlock>  );}

Examples

Hairline

Preview

Bare

Preview

Terminal with pane strip

Preview

Diff & Highlight states

Preview

Syntax highlighting

Preview

API Reference

CodeBlock

NameTypeDefaultDescription
variant"hairline" | "bare" | "terminal""hairline"Visual variant. "hairline" (default) is a soft-bordered block with a filename header. "bare" removes chrome and renders a 2px left rule that turns accent on hover; the header is suppressed. "terminal" centers the title in the header — use for shell output. The three-mark pane strip is opt-in via the `chrome` prop.
languagestringLanguage identifier exposed as data-language and used in the default aria-label ("{language} code").
labelstringOptional accessible name when no <CodeBlock.Label> is rendered. Falls back to "{language} code" or "Code block".
chrome"dots" | "none""none"Decorative chrome in the header strip. "none" (default) leaves the header to its label and actions. "dots" renders a three-mark pane strip on the left edge and reserves symmetric padding so a centered label stays balanced. The marks are hard 8px squares on a descending opacity ramp — a pane strip, not macOS window controls: nothing here closes, minimizes, or zooms.
childrenReactNodeHeader and Content subparts.

CodeBlockHeader

NameTypeDefaultDescription
childrenReactNodeTypically a CodeBlock.Label and optional action buttons. Returns null when the parent variant is "bare".

CodeBlockLabel

NameTypeDefaultDescription
childrenReactNodeFilename or language text. Bound to the figure's accessible name via aria-labelledby.

CodeBlockContent

NameTypeDefaultDescription
showLineNumbersbooleantrueAuto-split mode only. Renders a line-number gutter for string children.
wrapbooleanfalseSoft-wraps long lines instead of scrolling them horizontally. The line's flex row is the hanging indent, so continuation lines land under the code column, past the gutter. Use it for prose-like content in a code shell; leave it off for source, where indentation carries meaning.
childrenstring | ReactNodeCode source: a string is auto-split into numbered CodeBlock.Line children; composed CodeBlock.Line children render as-is.

CodeBlockLine

NameTypeDefaultDescription
numbernumber | nullLine number rendered in the gutter. `null` renders an empty gutter cell, for a row inside a numbered block that prints no line of its own (a gap or truncation marker), so the code beside it keeps its indent. Omit to render no gutter cell at all.
contentstring | { text: string; color?: string; className?: string }[]Line content. Either a plain string or an array of tokens for syntax coloring. Ignored when `children` is provided.
childrenReactNodePre-rendered line body (e.g. highlighted React elements). Takes precedence over `content` and renders inside the <code> element.
state"highlight" | "added" | "removed"Per-line visual state. "highlight" tints the row; "added"/"removed" render gutter sign characters (+/−), color tint, and an sr-only "Added: "/"Removed: " prefix for assistive tech.
addedLineLabelstring"Added: "Screen-reader prefix for an added diff line.
removedLineLabelstring"Removed: "Screen-reader prefix for a removed diff line.

CodeBlockCopyButton

NameTypeDefaultDescription
sourcerequiredstringText copied to the clipboard on click.
copyLabelstring"Copy code to clipboard"Accessible label for the button (overrideable for localization).
copiedMessagestring"Copied"Status message announced via aria-live after a successful copy.
copyFailedMessagestring"Copy failed"Status message announced via aria-live after a failed copy.
onCopy(source: string) => voidCalled after a successful clipboard write.
onCopyError(error: unknown) => voidCalled when the clipboard write fails or the API is unavailable.

CodeBlockHighlight

NameTypeDefaultDescription
lowlightrequiredLowlightInstanceCaller-created lowlight instance containing the language registrations this code block may use.
coderequiredstringSource code to highlight. Each newline becomes a separate row.
languagestringLanguage identifier consumed by lowlight (e.g. "ts", "tsx", "bash", "json"). Omit to use lowlight's auto-detection.
showLineNumbersbooleantrueRenders a line-number gutter when true.
wrapbooleanfalseSoft-wraps long lines instead of scrolling them horizontally. The line's flex row is the hanging indent, so continuation lines land under the code column, past the gutter. Use it for prose-like content in a code shell; leave it off for source, where indentation carries meaning.
lineStatesRecord<number, "highlight" | "added" | "removed">Optional per-line state map keyed by 1-based line number. Applied to the underlying CodeBlock.Line for each row.

Data attributes

AttributeApplies toValuesDescription
data-variantCodeBlock"hairline" | "bare" | "terminal"Visual chrome variant on the root figure.
data-chromeCodeBlock"dots" | "none"Decorative header chrome mode.
data-languageCodeBlocklanguage idLanguage identifier used by labels and syntax-highlighting selectors.
data-wrapCodeBlock.Content"on"Present when `wrap` soft-wraps long lines instead of scrolling them.
data-stateCodeBlock.Line"highlight" | "added" | "removed"Per-line visual state for highlights and diff rows.
data-stateCodeBlock.CopyButton"idle" | "copied" | "failed"Copy feedback state used for the button label, styling, and aria-live announcements.

Syntax highlighting

CodeBlock is highlighter-agnostic. Its core parts render structure — header, scrollable area, line numbers, copy button — but do not tokenize code. Pick one of three patterns to color output.

Pre-tokenized lines

Tokenize at build time and pass CodeBlockToken[] to each CodeBlockLine. Every span gets an inline color, so it renders without extra CSS:

tsx
import { CodeBlock, CodeBlockContent, CodeBlockLine } from "@/components/ui/code-block"

const lines = [
  [
    { text: "const", color: "var(--code-keyword)" },
    { text: " greeting", color: "var(--code-variable)" },
    { text: " = ", color: "var(--code-operator)" },
    { text: '"hello"', color: "var(--code-string)" },
  ],
]

<CodeBlock>
  <CodeBlockContent>
    {lines.map((tokens, i) => (
      <CodeBlockLine key={i} number={i + 1} content={tokens} />
    ))}
  </CodeBlockContent>
</CodeBlock>

This is the pattern used by <UsageSnippet /> above — tokens are generated at build time via Shiki, with colors mapped to CSS variables.

Shiki HTML with CSS variables

If a highlighter emits its own HTML — for example fumadocs-mdx or rehype-pretty-code with defaultColor: false — tokens arrive as spans that reference custom properties:

html
<span style="--shiki-light: var(--code-keyword)">const</span>

Add one CSS rule that resolves those properties to a real color, then apply the matching class to any ancestor inside CodeBlock:

css
.shiki span {
  color: var(--shiki-light);
  background-color: var(--shiki-light-bg);
}
tsx
pre: ({ children }) => (
  <CodeBlock>
    <CodeBlockContent className="shiki">{children}</CodeBlockContent>
  </CodeBlock>
)

.shiki is a descendant selector, so the class can sit on CodeBlockContent and apply to every token span the highlighter emits inside it. Define --code-keyword, --code-string, --code-comment, etc. in your theme to match your palette.

CodeBlockHighlight

Use CodeBlockHighlight when you want runtime syntax coloring through lowlight. Package consumers import it from @diffgazer/ui/components/code-block/highlight; copy, dgadd, and direct registry consumers add the separate ui/code-block-highlight item. Create a lowlight instance with the language set you need and pass it explicitly; the component does not load grammars at runtime.

tsx
import { CodeBlock } from "@diffgazer/ui/components/code-block"
import { CodeBlockHighlight } from "@diffgazer/ui/components/code-block/highlight"
import { common, createLowlight } from "lowlight"

const lowlight = createLowlight(common)

<CodeBlock aria-label="Example">
  <CodeBlockHighlight
    code="const greeting = 'hello'"
    language="typescript"
    lowlight={lowlight}
  />
</CodeBlock>

Accessibility

Keyboard Navigation

CodeBlock.Content uses ScrollArea for the inner region. When content overflows, users can Tab to the code region and scroll it with keyboard keys.

KeyAction
TabMoves focus to the scrollable code region or copy button.
Arrow / Page / Home / EndScrolls the focused code region through ScrollArea keyboard handling.
Enter / SpaceActivates CodeBlock.CopyButton when focused.

Notes

Variants

variant="hairline" (default) renders a 1px soft border with a header row for filename + actions. variant="bare" removes all chrome and renders a 2px left rule that turns accent on hover; the header is suppressed. variant="terminal" centers the title in the header for a shell pane. A three-mark pane strip is opt-in in every variant via chrome="dots". All chrome is driven by [data-variant] and [data-chrome] selectors in code-block/code-block.css; consumers do not need to apply any classes manually.

Compound API

CodeBlock is the root <figure>. CodeBlock.Header holds the filename label and inline actions. CodeBlock.Label renders the filename and is registered as the accessible name via aria-labelledby. CodeBlock.Content is the scrollable code body — pass a string for auto-line splitting, or map line data to CodeBlock.Line children. CodeBlock.CopyButton copies a string to the clipboard with an aria-live announcement. CodeBlockHighlight (imported from @diffgazer/ui/components/code-block/highlight) renders syntax-colored code with a required caller-created lowlight instance.

Accessible Name

Precedence: aria-labelledby > aria-label > <CodeBlock.Label> > `label` prop > "<language> code" > "Code block". When you render <CodeBlock.Label>, the figure picks it up automatically via an internal id.

Line States

Each CodeBlock.Line exposes a `state` prop: "added" tints the row with --success-subtle and renders a sr-only "Added: " prefix; "removed" tints with --error-subtle and a "Removed: " prefix; "highlight" tints with foreground color. The +/- gutter sign keeps the full-strength --success/--error tone. The tint is applied to the row, not the <code>, so syntax-color themes remain readable.

Injected Syntax Highlighting

CodeBlockHighlight is split from the main <CodeBlock> bundle so consumers who never render it are not charged for syntax-highlighting code. Package consumers import it from @diffgazer/ui/components/code-block/highlight. Copy, dgadd, and direct registry consumers add the separate ui/code-block-highlight item. Install the optional `lowlight` peer, create an instance with the desired language set, and pass it through the required `lowlight` prop. The component emits highlight.js-compatible class names (hljs-keyword, hljs-string, ...) which the shared CSS maps onto the --code-* theme tokens.

Keyboard Scrolling

CodeBlock.Content renders a scrollable region wired through ScrollArea. The inner scroller is keyboard-focusable (tabIndex=0) and exposes the figure's accessible name to screen readers; the scroller is the landmark (role=region), not the figure. Users can Tab to the code area and scroll with arrow keys when content overflows.

Token-based Highlighting

Line content can be a plain string or an array of CodeBlockToken objects ({ text, color?, className? }) for syntax-colored output. No HTML parsing — tokens render directly as React elements. Use `.code-*` class names to bind to the shared theme tokens.

Source

Install via CLI: pnpm exec dgadd add ui/code-block.

Highlighted source loads after this disclosure opens. Browse the source repository.