Code Block
A real <pre><code> inside a<figure>, with optional line numbers, emphasised lines, copy, and stable streaming. It highlights nothing itself — you hand it pre-tokenised lines — which is what keeps both packages dependency-free and lets one component serve Shiki, Prism, a server-side highlighter, or plain text.
Playground
Toggle every adjustable prop and watch the component — and the code — update live.
Usage
<CodeBlock
label="stickToBottom.ts"
language="ts"
showLineNumbers={true}
copyable={true}
highlightLines={[4]}
// or lines={await codeToTokens(source)} from your highlighter
code={source}
/><kernel-code-block
label="stickToBottom.ts"
language="ts"
show-line-numbers
highlight-lines="4"
><pre><code>const a = 1
const b = 2</code></pre></kernel-code-block>
<script type="module">
// Or hand it tokens from a highlighter:
document.querySelector("kernel-code-block").lines = [
{ tokens: [{ text: "const", color: "#c678dd" }, { text: " a = 1" }] },
];
</script>Props
| Prop | Type | Default |
|---|---|---|
lines | CodeLine[] | — |
code | string | "" |
language | string | — |
label | ReactNode | from language |
showLineNumbers | boolean | false |
highlightLines | number[] (1-based) | — |
streaming | boolean | false |
copyable | boolean | true |
maxBlockSize | string | — |
Bring your own tokens
A CodeLine is { tokens: CodeToken[] }, and aCodeToken is{ text, className?, color? } — className for highlighters that emit classes, color for the ones that emit inline colours. Shiki's codeToTokens maps over directly:
import { codeToTokens } from "shiki";
const { tokens } = await codeToTokens(source, { lang: "ts", theme: "github-dark" });
const lines = tokens.map((line) => ({ tokens: line }));
<CodeBlock lines={lines} language="ts" showLineNumbers />That's documentation, not a dependency —@kernelui-lib/react and@kernelui-lib/elements ship with none. Skip it entirely and pass code for unstyled source. In the elements package the same data arrives as a DOM property (el.lines = … orel.code = …), because structured data can't travel through an attribute; light-DOM<pre><code> written inside the tag is read once at connect, so the code is real page content before any script runs.
Stable streaming
Lines are keyed by index, so appending output re-renders only the last line. Earlier lines aren't unmounted and re-created — which is what causes the flicker and lost text selection you get from re-rendering a growing block as one string. Following the last line reuses the sameStickToBottomControllerthat Message Scroller is built on, rather than a second scroll implementation, and following is always instant: a smooth animation per chunk would never catch up with the next one.
Accessibility
- Line numbers are real text, but
aria-hiddenand unselectable. A screen reader reading "one const two import three…" is noise, and a copy that interleaves numbers with code is worse than no copy button at all. - Copy feedback is a live region, not just a changed icon — a button's own label changing is announced only if focus happens to be on it.
- Denied clipboard access (insecure context, blocked permission) fails silently on purpose: the code is on screen and selectable, so there's nothing for the reader to act on.