Skip to content
AstroCraft Docs
On this theme

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.

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.

NEXT STEPCommands & Testing