Code
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add display/codePlain-CSS theme — no build step
npx astrocraft-ui add display/code --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add display/code --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add display/code --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add display/code --theme tailwind --bridge lumosLive demo
Code, CodeBlock, Kbd
The block is focusable so it can be scrolled without a mouse, and the copy button announces its result through a live region outside itself — inside, its text would become part of the button's own name. Press Ctrl + C or use the button; on an insecure origin the clipboard is refused and it says so rather than doing nothing.
Inline, it looks like this: run pnpm headless before you call a change done.
pnpm add astrocraft-ui
import "astrocraft-ui/styles/structure.css";Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `Code.astro` | `code` | — | — | — |
| `CodeBlock.astro` | `code-block` `code-block-code` | `data-wrap`: `true` when set | — | — |
| `CodeCopyButton.astro` | `code-copy-button` `code-copy-status` | — | `data-copied` | `[hidden]` |
| `Kbd.astro` | `kbd` | `data-depth`: `single` · `chord` | — | — |
Source
What the command copies — 5 files, alias-free so the copy lands standing alone. This is the whole component; there is nothing else.
---
// src/components/ui/display/code/Code.astro — headless primitive (see ../../README.md).
// Inline `<code>`: a file name, a flag, an identifier in the middle of a sentence. One element and
// one attribute, and it is here for the same reason Badge is — so that a theme styles code ONCE,
// from `[data-slot="code"]`, instead of every consumer reaching for their own class.
//
// For a multi-line block, use CodeBlock: a `<pre>` is what preserves the whitespace, and a bare
// `<code>` collapses it.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"code">;
const { class: className, ...rest } = Astro.props;
---
<code class={className} data-slot="code" {...rest}><slot /></code>
---
// src/components/ui/display/code/CodeBlock.astro — Code compound part (see ../../README.md).
// `<pre><code>`, the pair the platform requires: `<pre>` preserves the whitespace, `<code>` supplies
// the semantics. Either alone is wrong, which is exactly the kind of thing a primitive should settle
// once.
//
// `tabindex="0"` IS THE POINT, and it is not decoration. A code block that overflows is a scrollable
// region, and a scrollable region that cannot be reached by keyboard hides its content from anyone
// not using a mouse — a WCAG 2.1.1 failure that is invisible in every review because the content is
// right there on screen. The cost is one extra tab stop per block, which is the trade every docs site
// that has thought about it makes. (Recent Chrome makes scroll containers focusable on its own; this
// covers every other engine, and costs nothing where it does not.)
//
// `label` additionally makes the block a NAMED region, so a screen reader announces what it is
// before reading it. It is opt-in rather than the default because `role="region"` is a landmark, and
// thirty landmarks called "Code" on a docs page is noise rather than help — name the blocks that
// matter.
//
// Highlighting is not ours. Pass already-highlighted markup into the slot (Shiki, Prism, a server
// renderer) and it lands inside the `<code>` untouched.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"pre"> & {
/** Names the block as a region — for the blocks worth announcing by name. */
label?: string;
/** Soft-wrap instead of scrolling. The theme reads `data-wrap`; this library sets no `white-space`. */
wrap?: boolean;
};
const { label, wrap = false, class: className, ...rest } = Astro.props;
---
<pre
tabindex="0"
role={label ? "region" : undefined}
aria-label={label}
class={className}
data-slot="code-block"
data-wrap={wrap ? "true" : undefined}
{...rest}><code data-slot="code-block-code"><slot /></code></pre>
---
// src/components/ui/display/code/CodeCopyButton.astro — Code compound part (see ../../README.md).
// The copy button on a code block. Three things make it behavior rather than a styled button, and
// all three are the things hand-rolled versions miss:
//
// • IT ANNOUNCES. Copying moves no focus and changes nothing on screen that a screen reader
// notices, so the usual "swap the icon to a tick" is a silent success. The status here is a live
// region OUTSIDE the button — inside, its text would become part of the button's own accessible
// name, and the control would rename itself to "Copy Copied" the moment it worked.
// • IT KEEPS FOCUS. The button stays focused and keeps its name, so a keyboard user can copy a
// second time without hunting for it. `data-copied` is the hook for a theme's tick glyph; it
// clears itself, because a button that says "Copied" forever is lying by the next paragraph.
// • IT HANDLES THE FAILURE. `navigator.clipboard` is undefined on a plain-http origin and can be
// refused by permissions policy in an iframe, so the promise rejects for reasons that have
// nothing to do with the user. A swallowed rejection is a button that does nothing, forever,
// with no clue why — so the catch announces the failure and marks `data-copied="false"`.
//
// It finds its block by walking up from itself to the nearest ancestor that contains one, so the
// ordinary case needs no wiring at all:
//
// <div>
// <CodeCopyButton />
// <CodeBlock>pnpm add astrocraft-ui</CodeBlock>
// </div>
//
// Point it somewhere explicitly with `for={id}` when the button is not near its block — a toolbar
// above a tabbed set of them, say.
import "../../../../styles/structure.css";
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"button"> & {
/** The `id` of the CodeBlock to copy. Omit to use the nearest one. */
for?: string;
label?: string;
/** Announced on success. */
copiedText?: string;
/** Announced when the clipboard refuses — an insecure origin, or a blocked permission. */
failedText?: string;
};
const {
for: controls,
label = "Copy code",
copiedText = "Copied",
failedText = "Copy failed",
class: className,
...rest
} = Astro.props;
---
<button
type="button"
aria-controls={controls}
aria-label={Astro.slots.has("default") ? undefined : label}
class={className}
data-slot="code-copy-button"
data-copied-text={copiedText}
data-failed-text={failedText}
{...rest}
>
<slot />
</button>
{/* The live region that speaks. Visually hidden by structure.css — the tick is the theme's job. */}
<span role="status" aria-live="polite" data-slot="code-copy-status"></span>
<script>
import { onReadyOnce } from "../../_once";
/** How long `data-copied` stays on the button — long enough to read, short enough not to lie. */
const FEEDBACK_MS = 2000;
/** The block this button copies: the one it points at, else the nearest one that contains it. */
function blockFor(button: HTMLElement): HTMLElement | null {
const id = button.getAttribute("aria-controls");
if (id) return document.getElementById(id);
for (let el = button.parentElement; el; el = el.parentElement) {
const found = el.querySelector<HTMLElement>('[data-slot="code-block"]');
if (found) return found;
}
return null;
}
function wire(button: HTMLElement) {
// The live region this button owns — its own next sibling, not a lookup that could find the one
// belonging to the next code block along.
const sibling = button.nextElementSibling;
const status =
sibling instanceof HTMLElement && sibling.dataset.slot === "code-copy-status"
? sibling
: null;
let clear: ReturnType<typeof setTimeout> | undefined;
const report = (ok: boolean) => {
button.dataset.copied = String(ok);
if (status) {
// Re-assigning the same string does not re-announce, so the region is emptied first.
status.textContent = "";
status.textContent = (ok ? button.dataset.copiedText : button.dataset.failedText) ?? "";
}
clearTimeout(clear);
clear = setTimeout(() => delete button.dataset.copied, FEEDBACK_MS);
};
button.addEventListener("click", async () => {
const block = blockFor(button);
// `navigator.clipboard` is undefined outright on a plain-http origin, so this is a check
// rather than a catch: reading `.writeText` off it would throw before any try block could
// mean anything.
if (!block || !navigator.clipboard) return report(false);
try {
await navigator.clipboard.writeText(block.textContent ?? "");
report(true);
} catch {
// Deliberately not rethrown: a refused clipboard is a normal condition of the page's origin
// or its permissions policy, not a bug to surface in the console. The user is told, which is
// what matters.
report(false);
}
});
}
onReadyOnce('[data-slot="code-copy-button"]', wire);
</script>
---
// src/components/ui/display/code/Kbd.astro — Code compound part (see ../../README.md).
// `<kbd>`: a key the reader is meant to press. Semantically distinct from `<code>` — that is text a
// machine reads, this is text a finger does — and the distinction is what lets a theme draw one as a
// keycap and the other as a monospace run.
//
// A chord is nested, per the spec: `<Kbd><Kbd>Ctrl</Kbd> + <Kbd>K</Kbd></Kbd>`. `data-depth` is
// there so a theme can draw the inner caps and leave the outer wrapper alone.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"kbd"> & { depth?: "single" | "chord" };
const { depth = "single", class: className, ...rest } = Astro.props;
---
<kbd class={className} data-slot="kbd" data-depth={depth} {...rest}><slot /></kbd>
import Code from "./Code.astro";
import CodeBlock from "./CodeBlock.astro";
import CodeCopyButton from "./CodeCopyButton.astro";
import Kbd from "./Kbd.astro";
export { Code, CodeBlock, CodeCopyButton, Kbd };
export default Code;
What you get
The component source, copied into your project by npx astrocraft-ui add display/code — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.