component
A syntax-highlighted source block, with optional line numbers, line highlighting, clamping and copy-to-clipboard.
Code does no highlighting of its own. It takes already-tokenized lines — { text, color?, className?, fontStyle? } runs — and renders them. That is the entire contract with a highlighter, and it is deliberate: Shiki alone is a multi-megabyte dependency with its own theme format, and baking it in would make every consumer of the design system pay for it whether or not they render code. Bring any tokenizer you like, or none.
Pass lines from a highlighter. @pindoba/core-code/shiki is an optional adapter that converts Shiki output into this shape — it ships as a separate entry point with shiki as an optional peer dependency, so importing Code never pulls in a highlighting engine:
import { codeToTokens } from "shiki";
import { shikiToLines } from "@pindoba/core-code/shiki";
const lines = shikiToLines(
await codeToTokens(source, { lang: "ts", theme: "github-dark" }),
);
Note the adapter converts Shiki’s output rather than importing Shiki, so it works with any Shiki version and runs happily on a server or in a worker. Pick a highlighter theme that matches your pindoba theme — Code never sees either, so it cannot reconcile them for you.
function greet(name) {
return "Hello, " + name;
}
---
import Code from "../Code.astro";
import { LINES } from "./_sample";
---
<Code lines={LINES} lang="ts" copyable />showLineNumbers adds a number beside each line, and startLine offsets the count so an excerpt can report its real position in the file rather than restarting at 1. highlightLines takes numbers in displayed terms, so it lines up with what the reader sees.
Numbers are per-line cells inside the code, not a separate gutter column: a real gutter has to stay scroll-locked to the code beside it and drifts the moment the block scrolls horizontally. They are also user-select: none, so a selection copies the code without dragging line numbers along.
function greet(name) {
return "Hello, " + name;
}
---
import Code from "../Code.astro";
import { LINES } from "./_sample";
---
<Code
lines={LINES}
lang="ts"
showLineNumbers
startLine={41}
highlightLines={[42]}
/>maxLines clamps a long block and fades the overflow; collapsible adds an expand control. Clamping is purely visual — every line is still rendered, so in-page search still finds text inside a collapsed block. A maxLines larger than the content is a no-op rather than a fade painted over nothing.
line 1 of a longer file
line 2 of a longer file
line 3 of a longer file
line 4 of a longer file
line 5 of a longer file
line 6 of a longer file
line 7 of a longer file
line 8 of a longer file
line 9 of a longer file
line 10 of a longer file
line 11 of a longer file
line 12 of a longer file
line 13 of a longer file
line 14 of a longer file
---
import Code from "../Code.astro";
const LONG = Array.from({ length: 14 }, (_, i) => ({
tokens: [{ text: `line ${i + 1} of a longer file` }],
}));
---
<Code lines={LONG} lang="txt" maxLines={5} collapsible showLineNumbers />Omit lines and the raw code is rendered as plain text, so a block degrades to something readable rather than to nothing. This is also the right choice when the source is not a supported language, or when the block is short enough that highlighting adds noise.
function greet(name) {
return "Hello, " + name;
}
---
import Code from "../Code.astro";
import { SOURCE } from "./_sample";
---
<Code code={SOURCE} />The block’s root is a Panel root, so it takes every Panel surface prop — background, emphasis, feedback, padding, radius (plus the per-side variants), border, shadow, translucent and as. The defaults reproduce the familiar block: a surface.hill fill, a muted 1px border and an sm corner.
radius="inner" is the concentric corner — it resolves against the nearest Panel ancestor’s radius minus its padding, so a block dropped inside a Card curves with it instead of against it.
The code surface is a Panel root. Inside a Card, radius="inner"replaces the default sm corner with the concentric one — the Card'sradius minus its content padding.
function greet(name) {
return "Hello, " + name;
}
Default radius="sm".
function greet(name) {
return "Hello, " + name;
}
radius="inner".
---
import Code from "../Code.astro";
import Card from "@pindoba/astro-card";
import { SOURCE } from "./_sample";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "lg", direction: "column", width: "full" })}>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
The code surface is a Panel root. Inside a Card, <code>radius="inner"</code>
replaces the default <code>sm</code> corner with the concentric one — the Card's
<code>radius</code> minus its content padding.
</p>
<Card size="md" radius="2xl" border="default">
<Code code={SOURCE} />
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Default <code>radius="sm"</code>.
</p>
</Card>
<Card size="md" radius="2xl" border="default">
<Code code={SOURCE} radius="inner" />
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
<code>radius="inner"</code>.
</p>
</Card>
</div>copyable renders a copy button in the toolbar. It copies the original source verbatim — reconstructed from the tokens when only lines were given — so what lands on the clipboard is never the rendered markup. The button’s state lives in its accessible name (Copy code → Copied), not only in a data attribute, because a button whose name never changes gives no feedback to a screen reader.
The scrolling region is focusable and labelled, so a horizontally overflowing block can be read without a pointer.
falseMark this as the chosen one in a set (a current nav item, a checked option, a selected card). Where `interactive` says it *can* be clicked, `active` says it *is* the current choice. Use `activeEmphasis` to control how loud the state is. Combined with `interactive`, hover and press restart their stepping from the active baseline rather than falling back to the unchosen ramp. The three values differ only in what drives the state: `true` by the prop (via `data-panel-active`), `"checked"` by a wrapped native input (`:has(input:checked)` — zero JS, nothing to re-render), and `"current"` by an existing `aria-current`.
the panel's `emphasis`How loud the `active` state is. Defaults to the panel's own `emphasis`, so the chosen state lands on the same ramp as the resting look — this prop is the override for when the active item should be louder (or quieter) than the panel itself. `"primary"` fills with the feedback accent ramp and flips text to the contrast scale (~150 RGB units of separation — the option that reads at a glance in a dense list). `"secondary"` tints the feedback surface ramp and `"tertiary"` the neutral ramp; both promote text to `bold` and lift the border to the default line, but move the surface only ~7–18 units.
"div"HTML element to render. Curated to container-like tags so semantic intent stays clear (no html/script/style/etc).
Surface background from the Panel scale (`surface.peak` → `surface.ground`, or `transparent`).
Border style: `none`, `default`, `bold`, or `muted`.
Border color while the surface is `active` — the explicit form of the ring `activeEmphasis` would otherwise pick. It holds through hover, so a `borderInteract` color can't steal it. `inherit` keeps whatever `activeEmphasis` chose; `none` clears it.
Border style applied on hover / focus interaction.
Raw source. Used verbatim when `lines` is absent, and always used as the text the copy button puts on the clipboard.
falseOffer an expand control when clamped by `maxLines`.
falseWhether the copy button is in its just-copied state. Frameworks own the timer that flips it back.
falseRender a copy-to-clipboard button in the toolbar.
Bound reference to the root element (`bind:this`). Typed as `HTMLElement` because the root is a Panel and `as` can change its tag.
Visual emphasis / prominence level.
falseWhether the block is currently expanded past `maxLines`. Frameworks feed their reactive state in; the component is controlled.
Semantic color tone: `neutral`, `primary`, `success`, `warning`, `danger`, or `inherit`.
Line numbers (in displayed terms, honouring `startLine`) to emphasise.
Enable interactive (hover / focus / press) affordances and states.
"Code block"Accessible label for the block.
Language label shown in the toolbar. Purely presentational — the component does no highlighting of its own.
Pre-tokenized lines from a highlighter. When omitted, `code` is rendered as unhighlighted plain text — so the component degrades to a readable block rather than nothing.
Clamp the block to this many lines, fading the overflow.
Called after the source has been written to the clipboard.
Inner padding from the spacing scale.
Per-slot style and HTML-attribute overrides.
Corner radius override from the spacing scale (or `full` for fully rounded).
Corner radius for the bottom-left and bottom-right corners.
Corner radius for the top-left and bottom-left corners.
Corner radius for the top-right and bottom-right corners.
Corner radius for the top-left and top-right corners.
falseSerialize the panel's resolved surface inputs into a `data-panel-config` attribute on the root, enabling runtime surface changes via `updatePanelElement()` (and, for inheritors like Card, `updateCard()`) without a framework runtime — the primary consumer is Astro / vanilla JS, where props don't re-render. Reactive frameworks (Svelte/React/Vue) don't need this: their prop changes re-run the connect already.
Keep (or drop) the always-1px transparent border the panel reserves so a border appearing or changing never shifts layout. Computed automatically — it's reserved when a resting `border` is visible, or when `interactive` / `borderInteract` / `active` can change the border at runtime. Set `false` only on a panel that must not occupy that 1px (e.g. a borderless housing frame that would otherwise add 2px around a set).
Elevation shadow applied to the surface.
falseShow a line number beside each line.
"sm"Type scale of the block. `xs` and `sm` currently render at the same font size — the scale's smallest step is `xs`, so separating them means moving `sm`/`md` up, which is a deliberate visual change that hasn't been made.
1Number the first line as this. Lets an excerpt report its real position in the file rather than restarting at 1.
Apply a frosted-glass effect with backdrop blur over a surface background.
falseWrap long lines instead of scrolling horizontally.
Plus all standard <div> HTML
attributes.