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 below lives in src/components/ui/<folder>/ and is imported from that folder:

import { Button } from "@components/ui/button";
import { Card, CardHeader, CardTitle } from "@components/ui/card";

Every primitive also accepts its element’s native attributes and a class that wins over the base through tailwind-merge.

Forty-four folders, forty-five components: PasswordInput and PasswordStrength share the password/ folder, since one is the meter for the other. Everything else is one folder per primitive.

Tier 1 — foundations

Primitive Parts Variants JS
Button Button variant primary/secondary/outline/ghost · size sm/md/lg · icon · renders <a> when given href
Input Input size sm/md/lg · state default/error/success
Label Label
Textarea Textarea size sm/md/lg · state default/error/success
Badge Badge variant primary/secondary/muted/outline/info/success/warning/error
Card Card CardImage CardHeader CardTitle CardDescription CardAction CardContent CardFooter variant default/elevated/outline/interactive · size sm/md/lg
Alert Alert variant info/success/warning/error
Separator Separator orientation horizontal/vertical
Skeleton Skeleton
Avatar Avatar size sm/md/lg

Input, Textarea, Select and AdvancedSelect all compose the same _field module, so the field look and the three validation states have one source of truth.

Tier 2 — disclosure and status

Primitive Parts Variants JS
Accordion Accordion AccordionItem AccordionTrigger AccordionContent
Tabs Tabs TabsList TabsTrigger TabsContent yes
Tooltip Tooltip side top/bottom/left/right
Breadcrumb Breadcrumb BreadcrumbItem BreadcrumbLink BreadcrumbPage BreadcrumbSeparator
Pagination Pagination PaginationItem PaginationLink PaginationEllipsis
Progress Progress size sm/md/lg
Spinner Spinner size sm/md/lg

Accordion is <details>. Tooltip is CSS-only. PaginationLink reuses the button config rather than redefining one. Tabs is the only one here shipping a script, and it degrades to all-panels-visible without it.

Tier 3 — overlays and controls

Primitive Parts Variants JS
Dialog Dialog DialogTrigger DialogClose DialogHeader DialogTitle DialogDescription DialogFooter yes
Sheet Sheet (+ SheetTrigger / SheetClose, re-exported from Dialog) side right/left/top/bottom yes
Dropdown Dropdown DropdownTrigger DropdownMenu DropdownItem yes
Select Select size sm/md/lg · state default/error/success
Checkbox Checkbox
Radio Radio
Switch Switch
Table Table TableHeader TableBody TableFooter TableRow TableHead TableCell TableCaption

Dialog and Sheet are native modal <dialog> — a Sheet is a Dialog pinned to an edge via its side variant. They share one delegated controller in _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, so it needs no astro:after-swap re-init. The entry and exit animations, the backdrop scrim and the modal scroll-lock live in _overlay.css as real @starting-style and allow-discrete transitions that honour prefers-reduced-motion.

Dropdown is the native Popover APIpopover="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 the menu under its trigger, reflows on scroll and resize, adds arrow-key roving (Up/Down/Home/End), and syncs aria-expanded, which is what flips the chevron. The trigger’s for matches the menu’s id.

Checkbox, Radio and Switch are native inputs styled appearance-none with peer and :checked. Switch hides its checkbox sr-only and drives a track and thumb off peer-checked. Zero JS.

Table is static <table> styling in a scroll wrapper. Sorting and data-grid behaviour are out of scope. If you put one in a scroll container, give it tabindex="0", role="region" and a label — an overflow box is not keyboard-focusable on its own in Chrome or Safari, so everything past its right edge is unreachable. The pricing comparison matrix does exactly this, and the ESLint config allows region for that reason.

Advanced form controls

Primitive Parts Variants JS
Slider Slider size sm/md
InputNumber InputNumber yes
ToggleCount ToggleCount ToggleCountValue yes
PasswordInput PasswordInput yes
PasswordStrength PasswordStrength yes
ComboBox ComboBox ComboBoxOption yes
AdvancedSelect AdvancedSelect size sm/md/lg · state default/error/success yes
Searchbox Searchbox SearchboxItem yes

Slider is a native <input type="range"> styled through the range pseudo-elements — a bg-muted track and a bg-primary thumb ringed in background, with Firefox also filling ::-moz-range-progress. Zero JS.

InputNumber is a native <input type="number"> between two steppers (reusing the button config); its script drives stepUp() / stepDown() and disables a stepper at its bound.

ToggleCount is a Monthly/Annual-style pricing toggle built on Switch — every ToggleCountValue for={id} swaps between its min and max text when the toggle flips.

PasswordInput reuses the input field and adds a show/hide button that flips type while keeping aria-pressed and aria-label accurate. PasswordStrength is a four-segment meter driven by the rule-based scorePassword in password/strength.ts, whose one runnable check is password/strength.test.ts.

ComboBox is a role="combobox" input over a filterable role="listbox"; arrow keys move the active option via aria-activedescendant and Enter commits.

AdvancedSelect is a searchable single or multi select backed by a real, visually-hidden native <select> for form submission. It requires JavaScript — the zero-JS alternative is the native Select.

Searchbox is a ⌘K command palette reusing the Dialog shell; its script filters items, adds arrow-key navigation and binds the global shortcut.

The filterable trio share _listbox.tsfilterByText, nextIndex, createActiveDescendant.

Primitive Parts Variants JS
Nav Nav NavItem NavLink Nav.orientation horizontal/vertical · NavLink.variant underline/slide/pills/segment
MegaMenu MegaMenu MegaMenuTrigger MegaMenuPanel MegaMenuItem MegaMenuPanel.columns 1/2/3 yes
List List ListItem marker none/disc/decimal · orientation vertical/horizontal · ListItem.icon
Marquee Marquee direction left/right/up/down · speed slow/normal/fast
ThemeToggle ThemeToggle reuses the button config yes

NavLink models active state with aria-current, zero JS — both spellings come from navCurrent in @js/nav, and the styling matches the attribute rather than its value.

MegaMenu is a Dropdown with a wide multi-column panel — the same move as Sheet reusing Dialog. It is placed by the same _popover.ts controller, and the trigger reuses the navLink config so it sits correctly between links in a Nav. The panel keeps natural Tab order rather than roving, because it is a grid of links and not a role="menu". Click to open only — hover triggers are hostile to touch and keyboard.

Marquee is a seamless pure-CSS infinite scroll. The slotted content renders repeat times as identical side-by-side tracks running one keyframe, each shifting a full copy width plus gap, so the loop has no seam. Copies after the first are aria-hidden — they are visual duplicates, and a screen reader must not read the logo wall three times. Its keyframes and --animate-marquee* tokens live in tailwind-theme.css, and --marquee-gap there must match the gap utility the component uses.

ThemeToggle’s sun/moon flip is CSS only, via the dark: variant, so it is correct pre-paint with no flash; only the click ships JavaScript. It is the one primitive that is not purely additive: it pairs with the inline pre-paint script in BaseHead, which reads localStorage("colorTheme"). That script must stay inline.

Motion primitives

Primitive Animates Driven by JS
Reveal one box native scroll timeline
CurtainReveal one box native scroll timeline
ImageReveal a photograph native scroll timeline
StaggerReveal a group of siblings IntersectionObserver + CSS yes
SplitReveal a string IntersectionObserver + CSS yes
CountUp a number IntersectionObserver + rAF yes
RollText a string :hover / :focus-visible

Reveal takes animation (eleven entrances) and range (entry cover contain gradual moderate brisk rapid); ImageReveal takes range with the first five of those.

All of them follow siteSettings.useAnimations with a per-call animate override, and all degrade to static under prefers-reduced-motion. Motion & Animation covers the two switches and the two gotchas — the StaggerReveal hidden-child trap and why ImageReveal is wrong for UI mocks.

The shared internal modules

Not primitives — a leading underscore says so, which is why they never appear in the inventory above.

_client.ts (the onReady re-init contract) · _dialog.ts · _popover.ts · _listbox.ts · _field.ts · _overlay.css · _Chevron.astro · password/strength.ts

Cards

Content-aware compositions in src/components/Cards/, built from the ui/card parts. Each takes a whole collection entry rather than its fields.

Card Takes Sidecar config
BlogCard CollectionEntry<"blog"> _blogCard.ts
CaseStudyCard CollectionEntry<"caseStudies"> — (no variants)
IntegrationCard CollectionEntry<"integrations"> _integrationCard.ts

_listingCard.ts holds the shape the two listing cards share.

Seeing them all

/examples/ui in dev renders every primitive in every variant across eight panels, plus the full icon registry and the motion catalog. About twenty of the 44 are used by no page in the theme — that is inventory, and this is how you shop it.

NEXT STEPCommands & Testing