Writing docs
Docs live in apps/docs/src/content/docs/**/*.mdx. Each page mirrors a feature in the system.
The :::example directive
Section titled “The :::example directive”A paired-tabs example with a rendered preview. The block accepts one html fence and one tsx fence; either may be omitted.
:::example
```html<button class="btn btn-primary">Save</button>```
```tsx<Button variant="primary">Save</Button>```
:::What happens at build time, from apps/docs/plugins/example/index.mjs:
- Both fences are run through
oxfmt(the prettier-compatible API), so source in MDX doesn’t need to be hand-aligned. - The
htmlfence drives the vanilla preview (set:html) and the Vanilla CSS code tab. - The
tsxfence is compiled into a default-exported React component, registered as a virtual module (virtual:example-preview/<hash>.tsx), hydrated as a client island, and shown when the React tab is selected. - Top-level
importstatements in the MDX are forwarded into the virtual module, so identifiers used in thetsxfence (<Button>,<IconPlus>) resolve the same way they do in the surrounding page.
Don’t author <Example> JSX directly — always use the directive.
React-only examples
Section titled “React-only examples”When a feature’s behavior lives in the React component (clipboard access, focus traps, anything driven by useState + an effect / timer) and the vanilla bundle can’t replicate it without consumer-written JS, drop the html fence so the example renders the React preview only. Mark the heading with Starlight’s <Badge>:
import { Badge as StarlightBadge } from "@astrojs/starlight/components";
### Copyable <StarlightBadge text="React only" variant="caution" />
:::example
```tsx<PropertyList> <PropertyList.Item label="EAN" value="…" copyable /></PropertyList>```
:::Alias to StarlightBadge when the page already imports the admin <Badge>. Keep the supporting CSS classes shipping in both bundles: consumers wiring their own vanilla JS still need the styling.
Astro imports in MDX
Section titled “Astro imports in MDX”Astro components (e.g. anything from @astrojs/starlight/components, or local .astro files) work in MDX prose but cannot live inside the React preview’s virtual module. The plugin skips .astro imports when forwarding — you get a build error if a tsx fence references an Astro component.
Callouts
Section titled “Callouts”<Callout variant="info" | "success" | "warning" | "danger"> (src/components/Callout.astro) renders the design system’s own .alert. The skill generator flattens it to a **Caution** — … lead-in.
Leave a blank line after the opening tag and before the closing one, so the body stays a markdown block at column 0:
<Callout variant="warning">
Set `aria-sort` on the sorted column **only**.
</Callout>Without the blank lines the body becomes JSX children, which oxfmt indents. That is harmless on screen, but it misindents any fenced code the callout wraps.
Starlight’s :::note / :::caution asides do not work here: they inject their icon as a raw-HTML mdast node, and the Sätteri processor escapes raw HTML instead of parsing it, so the icon renders as visible SVG source. generate-skill.mjs fails the build if a ::: marker survives, which catches an aside written by habit.
Don’t use the React <Alert> in prose either — it emits _ao--prefixed classes that only match inside ._ao-admin-root, and page prose has no such wrapper, so it renders unstyled.
Page shape
Section titled “Page shape”A component page is reference material with worked examples. Examples lead, because the rendered preview is the fastest way to find “the one that looks like my design”; the lookup table sits at the bottom, one click away in the table of contents.
---title: Badgesdescription: Compact status indicator for tags and counts.---
## Examples
### <Variation> ← heading, then the example. No intro prose.
### <Variation>
## Reference
### React
### Vanilla## Examplesis mandatory, with one###per variation. There is no page-level intro paragraph; the frontmatterdescriptionis the intro.## Referenceis mandatory on any page documenting a component.### Reactcarries props (and a parts table for compounds);### Vanillacarries classes and custom properties. Different people read the two tables, so restating one fact in both (size="sm"andselect-sm) is correct rather than duplication.- The Vanilla
Effectcolumn stands in for the preview. Because the agent skill bundle strips the rendered example, effects must be concrete: “1remtall,0.625remtext”, never “compact”. - Full-page assemblies belong in
modules/. A component page keeps only examples that teach its own component.
Writing the Reference
Section titled “Writing the Reference”- Don’t add a
Notescolumn. Starlight’s content column is max-width constrained, so a table wide enough for prose wraps one word per line. Keep tables toProp | Type | Default(plus a leadingPartcolumn for compounds) and put the caveats in a short paragraph underneath. - List only admin’s own props; close with “Plus native
<span>attributes.” rather than enumerating inherited ones. - Say when a default emits no class (
mdadds no class), and name classes that don’t exist but readers will guess (badge-neutral). - Write attributes bracketed —
[data-selected]— so they read as attributes and the docs checker doesn’t treat them as class names. - Run
pnpm check-docs. It fails on a class the component CSS doesn’t define, and reports how many component classes still have no Reference entry.
Prose style
Section titled “Prose style”Examples and the Reference carry the page; prose is for what neither can show.
- Any API fact belongs in the Reference table, so no prose goes under an example heading. A sentence survives only when it states something neither the example nor the table can: a browser caveat, a contrast failure, an a11y attribute you must hand-write in vanilla, or a scope boundary (“wire the filtering logic yourself”).
- Reach for
<Callout>when ignoring it breaks something: a required scrolling ancestor, a stacking-context rule, a missing accessible name. Scope boundaries get a plain short sentence instead. Callouts are heavy, and more than two or three on a page stops being a signal. - A heading carrying a
<StarlightBadge>can’t be a deep-link target. The JSX pushes a trailing hyphen into the slug (classnames-), and Sätteri’sheadingAttributesfeature, which would allow an explicit{#id}, is off. If another page needs to link to a section, leave the badge off and state “React only” in the body. - Keep the frontmatter
descriptionto one short sentence (≤ ~10 words), and don’t restate it as the body’s first paragraph.titleis sentence case (App shell,Dark mode,File inputs). - No marketing voice, and no rationale for third-party choices: name the library, link it, move on. Trust the reader knows
<details>,:has(),color-scheme. - Voice is flat and factual, with no rhetorical color. Cut antithesis (“X rather than Y”), flavor triads, anthropomorphism (“digits don’t shimmy”, “an irreversible action wants”), marketing intensifiers (“the fastest way”, “the simplest path”), empty reassurance (“text stays readable”, “visible at a glance”), and soft hedges (“most likely”).
- Bold marks a term being defined, never a lead-in. A list whose every item opens with a bolded phrase reads as generated; write the rule as the sentence itself.
- An em-dash needs to earn its place in a list or an aside that adds information. Prefer a colon, a semicolon, or a second sentence to a dash reached for by reflex, and don’t let one clause shape (
…, which is what X needs) become the house tic. - Consumer pages carry the contract; the plumbing stays here. Document the
_ao-scope, class-name parity, version pinning, and props, and leave the remark pipeline, virtual modules, and@layerordering to the contributing pages. - State a cross-cutting convention once.
className,classNames, theiconprop, the size scale, tones, and.Containerlive on Conventions. A component page restates the fact as a Reference row, never as a paragraph, and doesn’t append “See [Conventions]” to every section. - Keep cross-references tight:
See [Icons](../../basics/icons/).not “for the recommended library, sizing convention, and usage patterns.”
Linking
Section titled “Linking”The site is served from /admin-design-system/ on GitHub Pages, so absolute URLs must go through import.meta.env.BASE_URL:
<a href={`${import.meta.env.BASE_URL}components/buttons/`}>Buttons</a>In MDX body prose, prefer relative Markdown links so the source stays portable:
See [Icons](../../basics/icons/).Don’t hard-code /admin-design-system/... in MDX.
Regenerating the agent skill
Section titled “Regenerating the agent skill”The repo ships an agent skill bundle at skills/admin-design-system/. The bundle is generated: committed but produced by apps/docs/scripts/generate-skill.mjs walking every MDX page and writing per-page markdown plus a top-level SKILL.md.
After any change under apps/docs/src/content/docs/:
pnpm generate-skillCommit the updated skills/ alongside the MDX change in the same commit. CI runs the generator and fails on drift via git diff --exit-code -- skills.