Skip to main content
astrocraft-ui/ components · 101

Code

Free · MIT

Headless, 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

Install command
npx astrocraft-ui add display/code

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add display/code --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add display/code --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add display/code --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add display/code --theme tailwind --bridge lumos

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

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

ComponentSlotsVariantsRuntime stateNative 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
---
// 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
---
// 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
---
// 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
---
// 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>
src/components/ui/display/code/index.ts
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.