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;
}
---
// GENERATED from packages/demos/code/highlighted.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
const LINES = [
{
tokens: [
{ text: "function ", color: "#c678dd" },
{ text: "greet", color: "#61afef" },
{ text: "(" },
{ text: "name", color: "#e06c75" },
{ text: ") {" },
],
},
{
tokens: [
{ text: " return ", color: "#c678dd" },
{ text: '"Hello, "', color: "#98c379" },
{ text: " + " },
{ text: "name", color: "#e06c75" },
{ text: ";" },
],
},
{ tokens: [{ text: "}" }] },
];
---
<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;
}
---
// GENERATED from packages/demos/code/lineNumbers.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
const LINES = [
{
tokens: [
{ text: "function ", color: "#c678dd" },
{ text: "greet", color: "#61afef" },
{ text: "(" },
{ text: "name", color: "#e06c75" },
{ text: ") {" },
],
},
{
tokens: [
{ text: " return ", color: "#c678dd" },
{ text: '"Hello, "', color: "#98c379" },
{ text: " + " },
{ text: "name", color: "#e06c75" },
{ text: ";" },
],
},
{ tokens: [{ text: "}" }] },
];
---
<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
---
// GENERATED from packages/demos/code/clamped.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
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;
}
---
// GENERATED from packages/demos/code/plain.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
const SOURCE = `function greet(name) {
return "Hello, " + name;
}`;
---
<Code code={SOURCE} />The block’s root is a Panel root, so it takes every Panel surface prop — background, emphasis, tone, 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's radius minus its content padding.
function greet(name) {
return "Hello, " + name;
}
Default radius="sm".
function greet(name) {
return "Hello, " + name;
}
radius="inner".
---
// GENERATED from packages/demos/code/inner-radius.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
import Card from "@pindoba/astro-card";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const SOURCE = `function greet(name) {
return "Hello, " + name;
}`;
---
<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>title names the block — typically its filename. On a bare block it is the toolbar’s leading text, before the language label, and truncates rather than wrapping the one-line toolbar.
export async function retry<T>(task: () => Promise<T>, attempts = 3) {
for (let i = 1; ; i++) {
try { return await task(); }
catch (error) { if (i >= attempts) throw error; }
}
}
---
// GENERATED from packages/demos/code/titled.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
const RETRY_LINES = [
{
tokens: [
{ text: "export async function ", color: "#c678dd" },
{ text: "retry", color: "#61afef" },
{ text: "<T>(" },
{ text: "task", color: "#e06c75" },
{ text: ": () => Promise<T>, " },
{ text: "attempts", color: "#e06c75" },
{ text: " = " },
{ text: "3", color: "#d19a66" },
{ text: ") {" },
],
},
{
tokens: [
{ text: " for ", color: "#c678dd" },
{ text: "(let i = " },
{ text: "1", color: "#d19a66" },
{ text: "; ; i++) {" },
],
},
{
tokens: [
{ text: " try ", color: "#c678dd" },
{ text: "{ " },
{ text: "return await ", color: "#c678dd" },
{ text: "task", color: "#61afef" },
{ text: "(); }" },
],
},
{
tokens: [
{ text: " catch ", color: "#c678dd" },
{ text: "(error) { " },
{ text: "if ", color: "#c678dd" },
{ text: "(i >= attempts) " },
{ text: "throw ", color: "#c678dd" },
{ text: "error; }" },
],
},
{ tokens: [{ text: " }" }] },
{ tokens: [{ text: "}" }] },
];
---
<Code lines={RETRY_LINES} title="retry.ts" lang="ts" copyable />Code is card-optional: it renders bare by default, and a card object frames it in a Card. The block’s chrome then moves into the card’s anatomy:
title → the card heading (an h3 by default); lang → the eyebrow. The language reads as a small label about the heading, which is what an eyebrow is for — a subheading would read as a second line of prose under the filename.card.trailing you pass.card.footer.trailing.role="region") and the overflow fade → the card content, edge to edge (content.padding: "none"). Framed, the scroller’s focus ring is drawn inset, so neither the card nor an inset panel clips it.The block’s own surface props become the card’s defaults (a surface.hill shell, a muted border, an sm corner, the block’s size), with divided hairlines standing in for the old toolbar border. Every Card frame prop is accepted — card.heading / card.eyebrow override title / lang, and passThrough.card reaches the Card’s parts.
---
// GENERATED from packages/demos/code/card.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
const RETRY_LINES = [
{
tokens: [
{ text: "export async function ", color: "#c678dd" },
{ text: "retry", color: "#61afef" },
{ text: "<T>(" },
{ text: "task", color: "#e06c75" },
{ text: ": () => Promise<T>, " },
{ text: "attempts", color: "#e06c75" },
{ text: " = " },
{ text: "3", color: "#d19a66" },
{ text: ") {" },
],
},
{
tokens: [
{ text: " for ", color: "#c678dd" },
{ text: "(let i = " },
{ text: "1", color: "#d19a66" },
{ text: "; ; i++) {" },
],
},
{
tokens: [
{ text: " try ", color: "#c678dd" },
{ text: "{ " },
{ text: "return await ", color: "#c678dd" },
{ text: "task", color: "#61afef" },
{ text: "(); }" },
],
},
{
tokens: [
{ text: " catch ", color: "#c678dd" },
{ text: "(error) { " },
{ text: "if ", color: "#c678dd" },
{ text: "(i >= attempts) " },
{ text: "throw ", color: "#c678dd" },
{ text: "error; }" },
],
},
{ tokens: [{ text: " }" }] },
{ tokens: [{ text: "}" }] },
];
---
<Code
lines={RETRY_LINES}
title="retry.ts"
lang="ts"
copyable
showLineNumbers
highlightLines={[3]}
card={{}}
/>Set contentBackground (or card.content.background) to lift the code onto an inset panel inside the shell. The overflow fade follows whichever surface the code actually sits on — the shell when flush, the inset panel otherwise.
---
// GENERATED from packages/demos/code/card-inset.demo.tsx — edit the source, not this file.
import Code from "@pindoba/astro-code";
const MIGRATION_SOURCE = `CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
actor_id UUID NOT NULL REFERENCES users (id),
action TEXT NOT NULL,
target TEXT NOT NULL,
payload JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX audit_log_actor_idx ON audit_log (actor_id, created_at DESC);
CREATE INDEX audit_log_target_idx ON audit_log (target);`;
---
<Code
code={MIGRATION_SOURCE}
title="0042_audit_log.sql"
lang="sql"
copyable
maxLines={5}
collapsible
card={{
contentBackground: "surface.peak",
footer: { leading: "Applied to staging 2 hours ago" },
}}
/>card={false}Without a card prop (or with card={false}) the block renders exactly the pre-card DOM — the root data-component="code" with its toolbar. When framed, the root is the Card (data-component="card", data-card-kind="code"), and the clamp markers (data-clamped, --code-max-lines) move to the card content ([data-code-body]). Select either with :is([data-card-kind="code"], [data-component="code"]).
In Astro the card prop is string-only; rich header/footer content goes through the card-* slots (card-trailing, card-heading-leading, card-footer-leading…). The copy/expand boot script keeps working when framed: its marker rides the Card root.
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 tone to a screen reader.
The scrolling region is focusable and labelled, so a horizontally overflowing block can be read without a pointer.