Components Reference
The catalog. For the pattern behind it — the contract, overrides, the client lifecycle — see UI Components.
Everything here lives in src/components/ui/<name>/, exports a tv() config named after the component, and is demoed in /examples/ui/. Nineteen components ship a script; everything else is markup and CSS.
Statics
| Primitive | Parts | Variants |
|---|---|---|
| Button | — | variant primary/secondary/outline/ghost · size sm/md/lg · icon |
| Badge | — | variant |
| Alert | — | variant |
| Callout | — | tone |
| Card | Card, Image, Header, Title, Description, Action, Content, Footer | variant · size |
| Avatar | — | size |
| Separator | — | orientation |
| Skeleton | — | — |
| Spinner | — | size |
| Progress | — | size |
| Label | — | — |
| List | List, ListItem | marker none/disc/decimal · orientation |
| RuleList | RuleList, RuleListItem | columns · gap |
| Table | Table, Header, Body, Footer, Row, Head, Cell, Caption | — |
| CodeBlock | — | — · ships a script |
| Logo | — | size |
Button renders <a> or <button> depending on whether href is passed. List’s decimal marker renders an <ol>, and ListItem’s icon slot applies flex only when an icon is present so disc and decimal markers survive. Table is static table styling in a scroll wrapper; sorting and data-grid behaviour are out of scope.
Disclosure
| Primitive | Parts | Notes |
|---|---|---|
| Accordion | Accordion, Item, Trigger, Content | Native <details>. Zero JS. |
| Tabs | Tabs, List, Trigger, Content | One small script; degrades to all panels visible. |
| Tooltip | — | CSS-only. side variant. |
Overlays
| Primitive | Parts | Notes |
|---|---|---|
| Dialog | Dialog, Trigger, Close, Header, Title, Description, Footer | Native modal <dialog>. |
| Sheet | Sheet (+ Dialog’s parts, re-exported as SheetTrigger/SheetClose) | A Dialog pinned to an edge. side variant. |
| Dropdown | Dropdown, Trigger, Menu, Item | Native Popover API. |
| Searchbox | Searchbox, SearchboxItem | ⌘K palette reusing the Dialog shell. |
Dialog and Sheet share one delegated controller (_dialog.ts): openers carry data-dialog-open="<id>", closers data-dialog-close, plus backdrop light-dismiss. Escape is native. It binds once and survives view transitions with no re-init.
The fade/scale and per-side slide animations, the scrim and the modal scroll-lock live in _overlay.css as real @starting-style and allow-discrete transitions that honor prefers-reduced-motion.
Dropdown is popover="auto" plus popovertarget, so the menu renders in the top layer and is never clipped, with native light-dismiss, Escape and focus return. The shared _popover.ts controller positions it, reflows on scroll and resize, adds arrow-key roving and syncs aria-expanded.
Form controls
| Primitive | Parts | Variants / notes |
|---|---|---|
| Input | — | size. Shares _field.ts. |
| Textarea | — | size |
| Select | — | size. Native <select> with a token chevron. |
| Checkbox | — | Native input, appearance-none + peer. Zero JS. |
| Radio | — | as above |
| Switch | — | sr-only checkbox driving a track and thumb off peer-checked. Zero JS. |
| Slider | — | size. Native <input type="range"> styled through the range pseudo-elements. Zero JS. |
| InputNumber | — | Native number input between two steppers. Script. |
| ToggleCount | ToggleCount, ToggleCountValue | Built on Switch; each Value swaps its min/max text. Script. |
| PasswordInput | — | Show/hide button; script flips type and keeps aria-pressed accurate. |
| PasswordStrength | — | 4-segment meter over password/strength.ts. Script + a runnable check. |
| ComboBox | ComboBox, ComboBoxOption | role="combobox" over a filterable listbox. Script. |
| AdvancedSelect | — | size. Searchable single/multi select backed by a hidden native <select> for form submission. Requires JS — the zero-JS alternative is Select. |
| Chip | Chip, ChipRadio | active · focus self/peer |
Chip is one config serving two selection mechanisms. A blog category chip is a link to a real route, so the server knows which is current and active is a prop; the integrations wall filters in place, so its chips are a native radio group and the browser holds the state. The focus variant is what lets one config do both — a <label> never takes focus itself, so its ring hangs off the sr-only input beside it.
Navigation
| Primitive | Parts | Notes |
|---|---|---|
| Nav | Nav, NavItem, NavLink | orientation |
| MegaMenu | MegaMenu, Trigger, Panel, Item | Dropdown with a wide multi-column panel. columns 1/2/3. |
| NavHighlight | — | The sliding pill behind the active nav item. Script + geometry.ts and its check. |
| Breadcrumb | Breadcrumb, Item, Link, Page, Separator | — |
| Pagination | Pagination, Item, Link, Ellipsis | PaginationLink reuses the button config. |
| TocRail | — | The reading rail, driven by a named scroll timeline. Zero JS. |
| ThemeToggle | — | Icon flip is CSS-only (correct pre-paint, no flash); only the click ships JS. |
MegaMenu is click-to-open only — hover triggers are hostile to touch and keyboard, and the file says so. The panel keeps natural tab order rather than roving, because it is a grid of links and not a role="menu".
ThemeToggle is the one primitive that is not purely additive: it pairs with the inline pre-paint script in BaseHead, which reads the localStorage("colorTheme") key the toggle writes.
Page-level
| Primitive | Parts | Variants |
|---|---|---|
| Section | — | spacing none/sm/md/lg · clip · contained prop |
| SectionHead | — | width |
| HeroStage | — | Built on Section |
| HeroMedia | — | shape |
| NotchedCard | NotchedCard, NotchedCardBand | tone card/ink/brand · accent |
| MaskedImage | — | zoom |
| ArticleBand | — | — |
| PostCard | — | Content-unaware; fed by postCardProps |
| CtaButton | — | — |
| ArrowButton | — | size |
| ArrowLink | — | size · direction |
| RollText | — | — |
| Marquee | — | direction · speed |
NotchedCard is the design’s signature: a rounded rectangle whose bottom-right corner is cut away around a 56-pixel arrow button, so the button sits in the material. The shape is composed rather than drawn — border-radius gives the three ordinary corners and a mask subtracts a stamp pinned in pixels to the bottom-right, because the well must stay the same size at every card size. Two masked pseudo-elements give a hairline that follows the notch, since a mask clips a border away. tone="ink" and tone="brand" are pinned rather than themed, and ink carries text-foreground explicitly — the class re-points token variables, but color is an inherited computed value, so anything inside without an explicit colour class would keep light-mode ink and render invisible.
MaskedImage cuts a photograph to one of the design’s stepped silhouettes, with the picture free to scale inside a shape that holds still. The three paths are the exported boolean-operation unions verbatim, not traced. The frame holds the shape’s own aspect ratio, which is what makes one mask definition correct at every width. It deliberately has no clip: the mask already clips, and a clipped box is a scroll container that would freeze the view()-driven zoom at its first frame.
ArrowButton enforces two modes with a discriminated union rather than two optionals: href plus label renders a real <a> carrying its own accessible name, and neither renders an aria-hidden, pointer-events-none <span> for a card whose own stretched link already covers it. href without label does not compile — it used to, and quietly produced an unnamed link.
Motion
| Primitive | Parts | Notes |
|---|---|---|
| Reveal | — | animation (11 entrances) · scrubbed · range. Two triggers: scroll (zero-JS, scrubbed) and sight (real tween). |
| RevealGrid | RevealGrid, RevealItem | columns — items per row, so the stagger restarts each row. |
| SplitText | — | Per-character drop-in, split at build. Script for sight. |
| SentenceReveal | — | Per-clause drop-in, split at build. Script for sight. |
| CountUp | — | The one animation that genuinely needs JS. Server-renders the final value. |
All three text primitives take their text as a prop, not a slot, because they split at build time and the server needs the string. All follow siteSettings.useAnimations with a per-call animate override, resolved in one place by _motion.ts.
RevealItem sets its delay as an inline animationDelay rather than a class, which is what let a whole lookup-table module be deleted: a per-item delay is a value, not a style.
Icons
<Icon name="…" /> lives in src/components/svg/icons/ rather than in ui/, but follows the same contract: data-slot="icon", an exported tv() config, native svg props plus a size variant, merged class, tokens only. 571 icons, typed names. See Icons.
The shared internals
Not primitives — leading-underscore modules in src/components/ui/:
| Module | Owns |
|---|---|
_client.ts |
onReady — the load + astro:after-swap re-init contract, with an AbortSignal |
_dialog.ts |
the delegated Dialog/Sheet controller |
_popover.ts |
placement and keyboard for Dropdown and MegaMenu |
_field.ts |
the shared input look (size + validation state) |
_listbox.ts |
filterByText, nextIndex, createActiveDescendant |
_motion.ts |
animationsOn — the animate/useAnimations/default precedence |
_reveal.ts |
the play-on-first-sight controller |
_sequence.ts |
the stagger ladder (leadDelay) |
_zoom.ts |
zoomSizes for MaskedImage |
_overlay.css |
Dialog/Sheet transitions, scrim, modal scroll-lock |
_prose.css |
rendered-markdown typography |
_Chevron.astro |
the one disclosure/select chevron glyph |
_TitleLadder.astro |
the shared heading ladder |
Four of them carry runnable checks — _reveal.test.ts, _sequence.test.ts, _zoom.test.ts, and password/strength.test.ts, count-up/format.test.ts and nav-highlight/geometry.test.ts beside their primitives.
Which ones ship JavaScript
AdvancedSelect · CodeBlock · ComboBox · CountUp · Dialog · Dropdown · InputNumber · MegaMenu · NavHighlight · PasswordInput · PasswordStrength · Reveal · Searchbox · SentenceReveal · Sheet · SplitText · Tabs · ThemeToggle · ToggleCount.
Everything else is markup and CSS. If you are auditing bundle size, that list is where to start.
What is not used by any page
About twenty of the 61 are drawn by no page in the theme — CodeBlock, ComboBox, Searchbox, Slider, InputNumber and several others. That is inventory for the site you build next rather than dead weight, and /examples/ui/ is how you shop it. If you are certain you will not need one, deleting its folder and its catalog panel entry is a two-file change.