component

Code

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.

Highlighted

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.

ts

        
        
        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 />

Line Numbers and Highlighting

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.

ts

        
        
        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]}
/>

Clamping

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.

txt

        
        
        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 />

Without a Highlighter

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} />

Surface

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

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.

retry.tsts

        
        
        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 />

Card

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.
  • The copy button → the header’s trailing slot, after any card.trailing you pass.
  • The expand button → the footer’s trailing slot, after any card.footer.trailing.
  • The scroller (still a focusable, labelled 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.

ts

retry.ts


        
        
        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/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.

sql

0042_audit_log.sql


        
        
        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);
      
Applied to staging 2 hours ago
---
// 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" },
  }}
/>

Bare by default and 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.

Copying and Accessibility

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.

  • Components
  • Blocks