Code Block
Syntax-highlighted code surface powered by shiki. Lazy-loads token highlighting on mount, ships matched dark and light themes, and pairs with a context-aware copy button.
Overview
CodeBlock renders highlighted source code for any of shiki's bundled languages. It loads codeToHtml on mount (so the bundle stays small), produces both a light and a dark variant in parallel, and swaps between them via Tailwind's dark: selector instead of remounting.
A small React context exposes the raw code string to descendants. That lets CodeBlockCopyButton (and any custom child you drop into the absolutely-positioned action slot) copy the underlying source without prop drilling.
Use it for streamed code artifacts, MDX snippets inside tool output, agent-generated diffs and anywhere else you need an inline highlighted block.
Preview
Default
With copy button
Installation
bash npx gremorie@latest add rx-code-block bash pnpm dlx gremorie@latest add rx-code-block bash yarn dlx gremorie@latest add rx-code-block bash bunx --bun gremorie@latest add rx-code-block
Requires shiki and @gremorie/rx-forms (for Button) as transitive dependencies. Both are pulled in automatically by the registry.
Usage
import { CodeBlock, CodeBlockCopyButton } from "@gremorie/rx-ai";
export function Example({ source }) {
return (
<CodeBlock code={source} language="tsx" showLineNumbers>
<CodeBlockCopyButton />
</CodeBlock>
);
}import { Component, input } from "@angular/core";
import { CodeBlock, CodeBlockCopyButton } from "@gremorie/ng-ai";
@Component({
selector: "app-example",
standalone: true,
imports: [CodeBlock, CodeBlockCopyButton],
template: ` <code-block [code]="source()" language="tsx" [showLineNumbers]="true">
<code-block-copy-button />
</code-block>
`,
})
export class ExampleComponent {
readonly source = input.required<string>();
}
API
<CodeBlock>
| Prop | Type | Default | Description |
|---|---|---|---|
code | string | - | The source string to highlight. Required. |
language | BundledLanguage | - | Any shiki bundled language ("tsx", "json", "bash", "python", etc.). |
showLineNumbers | boolean | false | When true, prepends a muted line-number gutter via a shiki transformer. |
children | ReactNode | - | Rendered in an absolutely-positioned top-right slot. Use for the copy button or custom actions. |
className | string | - | Extra classes on the root div. |
Extends all HTMLAttributes<HTMLDivElement>.
Themes used internally:
- Light:
github-light-default(Odo final audit upgrade. The plaingithub-lighttheme missed AA on operator and punctuation tokens.) - Dark:
github-dark-defaultfor symmetric AA contrast.
<CodeBlockCopyButton>
| Prop | Type | Default | Description |
|---|---|---|---|
onCopy | () => void | - | Fires after a successful write to the clipboard. |
onError | (error: Error) => void | - | Fires when the Clipboard API is missing or rejected. |
timeout | number | 2000 | How long (ms) the button stays in the "copied" state before reverting. |
children | ReactNode | copy / check icon | Override the icon. The button still swaps to the check icon on success unless you fully replace it. |
Pulls the code string from CodeBlockContext, so it must be rendered inside a <CodeBlock>.
highlightCode(code, language, showLineNumbers?)
Async helper exposed for SSR / prerender flows that want to compute both light and dark HTML up front. Returns Promise<[lightHtml, darkHtml]>.
Composition
<CodeBlock>mounts a light and a dark<pre>block. Tailwind'sdark:selector flips visibility, so theme switches are instant (no re-highlight).- The
CodeBlockContextprovidescodeto descendants. - Children (typically
CodeBlockCopyButton) absolutely position into the top-right corner. The slot supports any node, so you can drop multiple buttons (e.g. copy + open in editor) by wrapping them in your own flex row. - Line numbers are produced by the
lineNumberTransformerso they participate in shiki's HTML output, not as a separate column.
Variations
With line numbers
Useful for long snippets where users need a reference point. The gutter is muted so it does not compete with the token colors.
<CodeBlock code={longSnippet} language="tsx" showLineNumbers />Copy plus custom action
The action slot accepts any node. Compose multiple buttons by nesting your own flex container.
<CodeBlock code={source} language="tsx">
<CodeBlockCopyButton />
<Button size="icon" variant="ghost" onClick={openInEditor}>
<ExternalLinkIcon />
</Button>
</CodeBlock>JSON for tool output
Pair with Tool. ToolInput and ToolOutput both render through CodeBlock, so JSON tool calls inherit the same highlighting and theming.
<CodeBlock code={JSON.stringify(toolResult, null, 2)} language="json" />Accessibility
- Keyboard: The copy button is a real
Button, so it participates in normal Tab order and responds to Enter / Space. - Contrast: Both
github-light-defaultandgithub-dark-defaultthemes clear WCAG AA 4.5:1 on every token. The Odo audit explicitly rejected the oldergithub-lighttheme because operator tokens dropped below the threshold. - Reduced motion: There is no animation on the surface itself. The copy button swaps between two icons without transition.
- Screen readers: When the copy button is icon-only, supply an accessible name through your own composition (e.g. wrap in a Tooltip with
aria-label). Internally the button is aghosticonButton, so the standard a11y rules for icon buttons apply.
Related
- Artifact - the card chrome that hosts a
CodeBlockfor generated code artifacts - Tool - uses
CodeBlockinternally to render tool input and output - Web Preview - the iframe surface paired with
CodeBlockfor live HTML preview