@stnd/ui
v0.26.0
Published
--- title: "@stnd/ui" aliases: [] created: 2026-07-05 07:47 modified: 2026-09-16T18:33:35.950Z last_audited: 2026-07-14 audit_interval_days: 90 next_audit: 2026-10-12 audit_priority: 3 maturity: sprout mode: read publish: true status: active tags: - pac
Readme
title: "@stnd/ui" aliases: [] created: 2026-07-05 07:47 modified: 2026-09-16T18:33:35.950Z last_audited: 2026-07-14 audit_interval_days: 90 next_audit: 2026-10-12 audit_priority: 3 maturity: sprout mode: read publish: true status: active tags:
- package
- stnd theme: maker type: package visibility: public garden-url: https://standard.garden/@francis/readme garden-short: https://stnd.gd/Yl18ps permalink: readme
@stnd/ui
Shared UI components for the Standard Ecosystem.
Most buttons/badges/cards should just be a CSS class on plain HTML (see @stnd/styles) — no JS, no import. This package is only for the handful of things that genuinely need interactivity: a dropdown that manages keyboard navigation, a confirm dialog you can await, a right-click menu. If you’re reaching for a component here and it’s not doing something stateful or ARIA-heavy, check whether a CSS class already does the job.
Use it (the confirm dialog, the most common one):
import { confirm } from "@stnd/ui/dialog.js";
const ok = await confirm({ title: "Delete this?", intent: "danger" });Overview
@stnd/ui provides a collection of reusable, accessible, and high-quality UI components built for Astro and Svelte 5. These components are designed to work seamlessly with @stnd/styles and follow the Standard design principles.
The Philosophy: CSS First, Components Second
Standard is built on the philosophy of keeping the codebase as clean, fast, and static as possible. Before importing a component from @stnd/ui, ask yourself: Can this be done with just HTML and @stnd/styles?
Many UI elements you see in the Playground (like Buttons, Badges, Cards, and Inputs) are not components. They are simple CSS classes applied to standard HTML elements.
Why?
- Zero JavaScript overhead.
- Perfect semantic HTML.
- Maximum flexibility without passing dozens of props.
Example: Buttons & Badges (CSS Only)
You do not need a <Button> or <Badge> component. Just use the global classes provided by @stnd/styles:
<!-- Primary Button -->
<button class="btn">Save Changes</button>
<!-- Ghost Button -->
<button class="btn ghost">Cancel</button>
<!-- Success Badge -->
<span class="badge success">Active</span>When to use @stnd/ui Components?
You should only import components from this package when you need:
- Complex interactivity (e.g.,
Dropdown,ContextMenu). - State management (e.g.,
DialogManager,ComboBox). - Advanced Accessibility / ARIA management (e.g.,
Accordion). - Complex composition (e.g.,
Accordion,Scroller,TableOfContents).
Icons and Lottie animations are not in this package — they live in
@stnd/icon(import Icon from "@stnd/icon/Icon.astro"). See its README.
Component API Reference
Astro Components (Zero JS)
<Header />
Minimalist responsive application and website header with brand wordmark, desktop menu, mobile navigation, and action slots.
brand(string): Brand text or logo label (defaults to site config title).brandHref(string): Link target for the brand.items(Array): Navigation items ({ title, url, external }).menu(boolean): Whether to render navigation menu and burger toggle.startHidden(boolean): When true, starts hidden and appears on scroll-up.
<Footer />
Flexible website footer with brand column, navigation links, copyright, and slots (brand, description, copyright, studio, bottom-left, bottom-right).
title(string): Brand or site name.hideBrand(boolean): Hides the brand column.hideNav(boolean): Hides the navigation links column.
<Accordion />
Accessible, collapsible content sections.
open(boolean): Sets the initial open state.
<Breadcrumb />
The trail to the current page. The last item is the current page: not a link, aria-current="page".
items({ label, href? }[]): The trail, from the root down.label(string): What the trail is called for screen readers (defaultBreadcrumb).
<Empty />
What a list or a page says when there is nothing to show yet.
title(string): The headline.icon(string): Optional icon above it.- Slots: the default slot is the explanation;
actionholds the way out (a button or a link).
<Stat />
One figure and what it measures.
label,value(string | number): The name and the figure.hint(string),change(string): A note under the figure, and the change since last time.tone(success|warning|error): Colours the figure when it says something about health.
No component needed: a progress bar is the native
<progress>, and a loading placeholder is the.skeletonclass (both live in@stnd/styles, see the forms and components pages).
Svelte 5 Components (Hydrated)
<Alert />
Semantic feedback banners.
class(string): The semantic intent:success,warning,error,info. (Defaults to accent color).title(string): Optional bold title.icon(string): Optional string icon (e.g.,✓,⚠).
<Alert class="warning" title="Watch out!" icon="⚠">
This action cannot be undone.
</Alert><Tabs />
A row of tabs and the panel of the selected one. Arrow keys, Home and End move between tabs; only the selected panel is rendered.
tabs({ id, label, icon?, disabled? }[]): The tabs.value(string, bindable): The id of the selected tab (defaults to the first enabled one).label(string): What the row is for, said to screen readers.- Snippets:
panel(tab)renders the content of the selected tab;endsits at the far end of the row.
<Tabs {tabs} bind:value label="Period">
{#snippet panel(tab)}...{/snippet}
</Tabs><OtpInput />
The boxes of a one-time code: focus moves on as you type, back on Backspace, a pasted code fills them all.
length(number, default 6),numeric(boolean, default true),label,disabled.value(string, bindable): The code so far.oncomplete(code): Called once the last box is filled.
<Tree />
A nested list that opens and closes by branch, on native <details>.
nodes({ id, label, href?, icon?, children? }[]): The tree.current(string): The id of the current node; its branches are open.onselect(node): Called when a node is chosen.
<Dropdown />
Accessible, keyboard-navigable dropdown menus.
align(string): Alignment of the menu (start,end). Defaults tostart.label(string): Recommended accessible name for the native trigger button. When supplied, thetriggersnippet contains only its content (not another button).triggerClass(string): CSS classes applied to the generated native trigger button.open(boolean, bindable): Controls or tracks the menu's open state.- Slots / Snippets:
trigger(the button content or trigger element),default(the menu items). - Keyboard & Dismissal:
ArrowUp/ArrowDownandHome/Endmove focus among enabled items.Escapedismisses the menu and restores focus to the trigger.Tabdismisses the menu and continues normal focus navigation.- Item activation closes the menu automatically without double-toggling native button triggers.
Related Sub-components:
<DropdownItem icon="ph:user" shortcut="⌘P">Profile</DropdownItem><DropdownLabel label="My Account" /><DropdownSeparator />
<ContextMenu />
Viewport-aware right-click menus.
- Slots / Snippets:
default(the area that triggers the menu on right-click),content(the menu items). - Controlled / Delegated Mode: For virtualized or delegated targets, omit the
defaultsnippet and passopen(bindable),position={{ x, y }}(viewport coordinates),label, andonclose. - Viewport Clamping: Measures actual menu dimensions, clamps coordinates to stay within the viewport, and scrolls oversized menus automatically.
- Keyboard & Dismissal:
ArrowUp/ArrowDownandHome/Endmove focus among enabled items.Escape,Tab, outside clicks, viewport resize, and outside scroll dismiss the menu and restore focus when appropriate.- Context-menu item components synchronize both native
disabledattributes andaria-disabled.
<DataTable />
A table whose column headers sort on click. Click a header (or press Enter/Space on it) to cycle ascending → descending → original order. Output is a plain <table> styled by @stnd/styles; the sort state is exposed through aria-sort and announced politely to screen readers.
<DataTable
{rows}
rowKey="id"
locale="fr"
columns={[
{ key: "name", header: "Station" },
{ key: "bikes", header: "Bikes", numeric: true },
{ key: "trend", header: "Trend", sortable: false },
]}
>
{#snippet cell({ row, column, value })}
{#if column.key === "trend"}<Sparkline data={row.trend} />{:else}{value}{/if}
{/snippet}
</DataTable>rows(array) andcolumns({ key, header, sortable = true, numeric = false, accessor?, class? }[]).accessor(row)sorts and renders computed values.rowKey(string or(row) => key): stable row identity, so selection and DOM state survive a re-sort. Falls back to the index.sort(bindable{ key, direction }, directionascending | descending | none): start sorted or persist the choice.locale: collation for text ("fr"ignores accents and sorts "10" after "9"). Empty values (null,undefined,"") always sort last.selectedKey+onrowclick(row): makes rows focusable and clickable (Enter/Space), highlights the selected one. Links and buttons inside a row keep their own behaviour.labels({ ascending, descending, none }) translate the screen-reader announcement;caption,class(defaults tostriped).- Snippets:
cell({ row, column, value })for custom cells,header({ column })for custom header content,empty()for the full-width row shown when there are no rows. manualSort: you orderrowsyourself (paginated or server-side lists, where sorting only the visible page would be wrong). Bindsortand apply it withsortRowsfromsort.jsbefore slicing the page.
The comparison logic lives in sort.js (sortRows, compare, nextDirection) and works on any array without the component.
<Dialog />
A general-purpose native modal shell for forms and arbitrary content. This complements AlertDialog and confirm() without imposing confirmation semantics.
<Dialog bind:open label="Preferences" onclose={discardDraft}
style="--dialog-width: 580px; --dialog-max-height: 88vh">
<!-- Form content and semantic buttons belong to the consumer. -->
</Dialog>- Props:
open(boolean, bindable): Controls modal visibility via nativeshowModal().label(string, required): Accessible dialog name (aria-label).children(Snippet): Modal body content.class/style(string): Customizes the<dialog>shell (e.g.--dialog-width,--dialog-max-height).onclose/onkeydown(function): Lifecycle and content-specific keyboard callbacks.
- Behavior & Accessibility:
- Manages native
showModal(), background inertness, focus trapping,Escape/ backdrop dismissal, and focus restoration. - Stops keyboard event propagation from reaching global application shortcut listeners.
- Intended for modal interactions (not non-modal floating monitors or OS windows).
- Manages native
<Popover />
An anchored, non-modal popup for forms and mixed controls. Use Dropdown for action menus instead.
<Popover label="Display options" align="end">
{#snippet trigger(attributes)}
<button class="btn ghost" {...attributes}>Display</button>
{/snippet}
<!-- Native inputs and content-specific buttons. -->
</Popover>- Props:
label(string, required): Accessible name for the popover region.open(boolean, bindable): Controls or tracks open state.trigger(attributes)(Snippet): Spreadattributesonto a single native<button>to wire click handling and ARIA relationships.children(Snippet): Popover body content.align("start" | "end") andside("bottom" | "top"): Anchor placement relative to the trigger.class(string) andonclose(function): Optional wrapper class and close callback.
- Keyboard & Dismissal:
Escapedismisses the popover and restores focus to the trigger.- Clicking outside or moving focus out of both the popup and trigger dismisses the popover.
- Keyboard events inside the popover do not leak to global application shortcuts.
- Validation & Testing:
- Shared menu geometry and navigation helpers are tested via
node --test packages/ui/menu.test.js. - See Reveal UI validation for browser regression coverage across context menus, dropdowns, popovers, and dialogs.
- Shared menu geometry and navigation helpers are tested via
Dialog Manager
The DialogManager allows you to trigger accessible confirmation dialogs programmatically from any script using a simple Promise-based API. This prevents littering your DOM with hidden modal HTML.
AlertDialog can also be composed directly inside an existing Dialog when confirmation belongs to that form. Its title provides the accessible dialog name, keyboard events stay within the confirmation instead of reaching parent form shortcuts, and a programmatic close after confirmation does not also call the cancellation callback.
1. Mount the Manager
Ensure <DialogManagerComponent client:load /> is mounted once in your root layout (Standard does this by default).
2. Call the API
import { confirm } from "@stnd/ui/dialog.js";
const isConfirmed = await confirm({
title: "Delete this item?",
description: "This action cannot be undone. Are you sure?",
confirmLabel: "Yes, Delete",
cancelLabel: "Cancel",
intent: "danger" // "neutral" | "danger"
});
if (isConfirmed) {
// Proceed with deletion
}Notes / Observations
(jot down anything noticed here — quirks, gotchas, ideas)
Todo
- [ ] README still lacks
Combobox,Pagination,Scroller,TableOfContents,Toast,CfImage,LauncherHintandColorModeToggle. Each has an@componentblock in its source; copy it here. [priority:: 3] [token_scale:: 3] [created:: 2026-07-14] [area:: framework]
