Components
@zerotal/flow-ui ships 53 themeable components for Flow. They are
built on accessible headless primitives and design tokens, so they follow your theme
(light / dark) out of the box. Add them with the CLI (copy the source into your app) or
import them straight from the package.
The catalogue covers what an application actually needs rather than only the obvious primitives:
| Group | Components |
|---|---|
| Forms | Field, Input, InputGroup, InputOTP, Textarea, Select, Combobox, Checkbox, RadioGroup, Switch, Slider, Toggle, ToggleGroup, Label, Calendar, DatePicker |
| Buttons & actions | Button, ButtonGroup, DropdownMenu, ContextMenu, Menubar |
| Overlays | Dialog, AlertDialog, Sheet, Popover, HoverCard, Tooltip, Command |
| Navigation | Sidebar, NavigationMenu, Breadcrumb, Pagination, Tabs |
| Data | Table, Chart, Item, Avatar, Badge |
| Feedback | Toaster, Alert, Progress, Spinner, Skeleton, Empty |
| Layout & content | Card, Accordion, Collapsible, ScrollArea, Resizable, Carousel, AspectRatio, Separator, Prose, Kbd |
Where the work happens
The components follow one rule, and it is worth stating before the API: the server renders, the client reacts.
Anything that answers to a pointer or a keystroke — dragging a slider, paging a calendar, filtering a palette, pressing a toggle, hovering a chart — runs entirely in the browser through Alpine. Those interactions have to land inside a frame, and a network round-trip cannot make that promise. Putting them on the server produces the specific ugliness of a control that lags its own input: a slider whose readout does not move while you drag it, a segmented button that stays unpressed until the reply arrives.
The server's job is the first paint and the data. It renders the initial state so the page is correct and complete before any script runs, and it hears about a change once — when there is something to persist. A date picker syncs the day you chose, not the six months you browsed to find it.
In practice that means most components bind rather than call:
<Slider bind={this.volume} showValue /> {/* live while dragging, synced on release */}
<ToggleGroup bind={this.view} options={…} /> {/* presses instantly, syncs after */}
<DatePicker bind={this.due} /> {/* months page client-side, day syncs once */}
Where a component genuinely needs the server — a calendar laying out records, a table of rows — it says so, and paging is a real navigation because the next page means different data.
Getting Started
# in your project root
bun add @zerotal/flow-ui
Register the provider
FlowUiProvider is a scaffolding-only provider: it registers the flow:* CLI commands. The components themselves are plain functions with no runtime service, so you only need the provider to use the CLI. Add it to the providers array in bootstrap/providers.ts:
// bootstrap/providers.ts
import { FlowUiProvider } from "@zerotal/flow-ui";
const providers = [
// …your other providers
FlowUiProvider,
];
export default providers;
Registering the provider switches on the following:
onBooted— registers theflow:list,flow:add, andflow:initconsole commands (only when thecommandsbinding is present, so web/test boots stay unaffected).
Set up the theme
Run flow:init once per app. It drops the shared cn / gva utilities into app/flow/components/ui/lib/ and wires the design-token theme into your Tailwind entry CSS:
# in your project root
bun zt flow:init
Note —
flow:initis idempotent: it skips files that already exist and only adds the@import "@zerotal/flow-ui/theme.css";line if it is missing. Pass--css <path>to target a Tailwind entry other thanresources/css/app.css.
Without a build step
Components are written against design tokens — bg-card, text-muted-foreground, border-input, bg-primary — so defining those tokens themes the whole kit at once. theme.css does that for a real Tailwind build. When you have no build step, flowUiHead() does the same job as a <head> payload: it loads the Tailwind Play CDN, configures it with the identical token mapping, and emits the palette inline.
import { Layout } from "@zerotal/flow";
import { flowUiHead } from "@zerotal/flow-ui";
export class AppLayout extends Layout {
static override get head() {
return flowUiHead("Acme");
}
}
Re-brand by passing tokensCss; it is appended after the defaults, so overriding one variable recolours everything that reads it:
flowUiHead("Acme", {
tokensCss: `
:root { --primary: 21 90% 48%; --ring: 21 90% 48%; }
.dark { --primary: 25 95% 58%; --ring: 25 95% 58%; }
`,
});
This is the path both panels Zerotal ships take. The admin uses it through adminHead(), and the monitor uses it directly with an orange --primary — which is the whole reason the two look like the same product rather than two.
The token set
Alongside the base tokens (background, card, primary, muted, accent, destructive, border, input, ring) the theme adds:
| Token | For |
|---|---|
success / warning | The states destructive doesn't cover — "healthy" and "degraded". |
flow-toast-success / flow-toast-warning | Status accents the toast host reads at runtime. |
chart-1 … chart-7 | A categorical series palette for charts. |
The chart tokens are deliberately not semantic: "the second line" isn't good or bad, it just has to stay distinguishable from the first. Because SVG stroke and fill can't take Tailwind classes, reference them as hsl(var(--chart-2)).
Adding components
flow:add copies component source into app/flow/components/ui/, so you own the code
outright. The shared cn / gva utils land in ui/lib/, and util imports are rewritten
to the local ./lib/* paths on the way in.
A component built from others brings them along. Asking for date-picker also copies
popover and calendar, because a file importing a Calendar that was never copied is
not a working component.
# in your project root
bun zt flow:add button # one component
bun zt flow:add button,card,dialog # several, comma-separated
bun zt flow:add --all # everything
bun zt flow:add button --force # overwrite if it exists
Browse everything available with flow:list:
# in your project root
bun zt flow:list
Tip — The copied
cnutil needsclsxandtailwind-mergein your app.flow:addandflow:initwarn you with the exactbun addcommand if they are missing.
Copy-in vs import — which should I use?
Both give you the same components; they differ in ownership:
- Copy in with
flow:addwhen you want to own and tweak the source — change variants, restyle, or extend a component. The code lives in your repo and won't change under you. - Import from
@zerotal/flow-uiwhen you just want the component as-is and prefer to track upstream updates. Every component is exported from the package root:
// in a Flow component
import { Button, Card, Dialog } from "@zerotal/flow-ui";
The component API is identical either way — the sections below apply to both.
Props not listed for a component pass straight through to the underlying element —
onClick, type, disabled, and any flow:* directive behave exactly as they would
on the raw tag, so a component never gets in the way of Flow's own bindings.
All 53 components
- Button — Clickable button with variants + sizes
- Badge — Small status pill with variants
- Card — Surface container (+ header/title/content/footer)
- Input — Themed text input (two-way bindable)
- Textarea — Themed multi-line input
- Label — Form label (wraps the headless Label)
- Separator — Horizontal/vertical divider
- Skeleton — Pulsing loading placeholder
- Avatar — Circular avatar with image + fallback
- Switch — On/off toggle bound to a boolean
- Checkbox — Checkbox bound to a boolean
- Select — Native select bound to a value
- RadioGroup — Segmented radio set bound to a value
- Dialog — Modal dialog (focus-trapped)
- Sheet — Edge-anchored slide-over panel
- DropdownMenu — Keyboard-navigable menu (+ item/label/separator)
- Tabs — Tabbed panels with a pill tablist
- Alert — Inline alert (+ title/description)
- Tooltip — Hover/focus tooltip
- Table — URL-sortable data table
- Popover — Floating panel anchored to a trigger
- HoverCard — Preview panel shown on hover
- AlertDialog — Confirm before something irreversible
- Command — Searchable command menu (⌘K palette)
- ContextMenu — Right-click menu
- Menubar — Application menu bar
- NavigationMenu — Site nav with dropdown panels
- Sidebar — App shell nav rail with a mobile drawer
- Breadcrumb — Trail showing where a page sits
- Pagination — Page links with a windowed number range
- Field — Label + control + description + error
- InputGroup — Input with affixes or addons
- InputOTP — One-time-code input
- Combobox — Autocomplete over many options
- Slider — Value chosen from a range
- Toggle — Pressed-state button (+ toggle group)
- ButtonGroup — Buttons joined into one control
- Calendar — Month grid for picking or laying out dates
- DatePicker — Calendar in a popover
- Toaster — Host for transient flash messages
- Progress — Determinate progress bar
- Spinner — Indeterminate loading indicator
- Empty — Empty-state block with an action
- Kbd — Keyboard key (+ platform modifier)
- Accordion — Stacked collapsible sections
- Collapsible — One section that opens and closes
- ScrollArea — Scrollable region with a styled bar
- Resizable — Two panes with a draggable handle
- Carousel — Snap-scrolling strip with controls
- AspectRatio — Fixed width-to-height box
- Item — Icon + title + description + action row
- Chart — SVG line, area, bar and donut charts
- Prose — Prose wrapper + heading helpers
Button
Clickable button with variants + sizes.
Button installation
bun zt flow:add button
Or import directly from the package: import { Button } from "@zerotal/flow-ui";
Button preview
Button usage
<Button onClick={this.save}>Save</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
Button props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | "default" | Visual style. |
size | "default" | "sm" | "lg" | "icon" | "default" | Sizing. |
onClick | handler | — | Server action or client expression (standard Flow). |
class | string | — | Extra classes, merged last (wins over defaults). |
Badge
Small status pill with variants.
Badge installation
bun zt flow:add badge
Or import directly from the package: import { Badge } from "@zerotal/flow-ui";
Badge preview
Badge usage
<Badge>New</Badge>
<Badge variant="secondary">Beta</Badge>
<Badge variant="destructive">Overdue</Badge>
<Badge variant="outline">Draft</Badge>
Badge props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "destructive" | "outline" | "default" | Visual style. |
class | string | — | Extra classes. |
Card
Surface container (+ header/title/content/footer).
Card installation
bun zt flow:add card
Or import directly from the package: import { Card } from "@zerotal/flow-ui";
Card preview
Create project
Deploy your new project in one click.
Project settings go here.
Card usage
<Card>
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one click.</CardDescription>
</CardHeader>
<CardContent>…</CardContent>
<CardFooter>
<Button>Deploy</Button>
</CardFooter>
</Card>
Card props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | — | Extra classes on the surface. |
children | node | — | Compose with CardHeader / CardTitle / CardDescription / CardContent / CardFooter. |
Input
Themed text input (two-way bindable).
Input installation
bun zt flow:add input
Or import directly from the package: import { Input } from "@zerotal/flow-ui";
Input preview
Input usage
<Field label="Email">
<Input value={this.form.email} placeholder="you@example.com" />
</Field>
Input props
| Prop | Type | Default | Description |
|---|---|---|---|
value | bound state | — | Two-way bind to an @expose / form field (emits flow:model). |
type | string | "text" | Native input type. |
class | string | — | Extra classes. |
Textarea
Themed multi-line input.
Textarea installation
bun zt flow:add textarea
Or import directly from the package: import { Textarea } from "@zerotal/flow-ui";
Textarea preview
Textarea usage
<Textarea value={this.form.bio} placeholder="Tell us about yourself" rows={4} />
Textarea props
| Prop | Type | Default | Description |
|---|---|---|---|
value | bound state | — | Two-way bind to an @expose / form field. |
class | string | — | Extra classes. |
Label
Form label (wraps the headless Label).
Label installation
bun zt flow:add label
Or import directly from the package: import { Label } from "@zerotal/flow-ui";
Label preview
Label usage
<Label for="email">Email</Label>
Label props
| Prop | Type | Default | Description |
|---|---|---|---|
for | string | — | Associated control id. |
class | string | — | Extra classes. |
Separator
Horizontal/vertical divider.
Separator installation
bun zt flow:add separator
Or import directly from the package: import { Separator } from "@zerotal/flow-ui";
Separator preview
Radix Primitives
An open-source UI component library.
Separator usage
<Separator />
<Separator orientation="vertical" class="h-6" />
Separator props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Divider direction. |
decorative | boolean | true | ARIA-hidden when decorative; semantic separator otherwise. |
Skeleton
Pulsing loading placeholder.
Skeleton installation
bun zt flow:add skeleton
Or import directly from the package: import { Skeleton } from "@zerotal/flow-ui";
Skeleton preview
Skeleton usage
<Skeleton class="h-12 w-12 rounded-full" />
<Skeleton class="h-4 w-48" />
Skeleton props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | — | Size + shape via utilities. |
Avatar
Circular avatar with image + fallback.
Avatar installation
bun zt flow:add avatar
Or import directly from the package: import { Avatar } from "@zerotal/flow-ui";
Avatar preview
Avatar usage
<Avatar src={user.avatarUrl} alt={user.name} fallback="AL" />
<Avatar fallback="GH" />
Avatar props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | null | — | Image URL; falls back to `fallback` when absent. |
fallback | node | — | Shown when there's no image (e.g. initials). |
alt | string | — | Image alt text. |
Switch
On/off toggle bound to a boolean.
Switch installation
bun zt flow:add switch
Or import directly from the package: import { Switch } from "@zerotal/flow-ui";
Switch preview
Switch usage
<Switch bind={this.notifications} />
Switch props
| Prop | Type | Default | Description |
|---|---|---|---|
bind | @expose boolean | — | Two-way bound boolean (server-synced). |
class | string | — | Extra classes on the track. |
Checkbox
Checkbox bound to a boolean.
Checkbox installation
bun zt flow:add checkbox
Or import directly from the package: import { Checkbox } from "@zerotal/flow-ui";
Checkbox preview
Checkbox usage
<Checkbox bind={this.agree} />
Checkbox props
| Prop | Type | Default | Description |
|---|---|---|---|
bind | @expose boolean | — | Two-way bound boolean. |
class | string | — | Extra classes. |
Select
Native select bound to a value.
Select installation
bun zt flow:add select
Or import directly from the package: import { Select } from "@zerotal/flow-ui";
Select preview
Select usage
<Select bind={this.country} options={[{ label: "Canada", value: "ca" }]} />
Select props
| Prop | Type | Default | Description |
|---|---|---|---|
bind | @expose value | — | Two-way bound value (flow:model). |
options | { label, value }[] | — | Option list. |
placeholder | string | — | Optional empty first option. |
RadioGroup
Segmented radio set bound to a value.
RadioGroup installation
bun zt flow:add radio-group
Or import directly from the package: import { RadioGroup } from "@zerotal/flow-ui";
RadioGroup preview
RadioGroup usage
<RadioGroup bind={this.plan} options={[{ label: "Pro", value: "pro" }]} />
RadioGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
bind | @expose value | — | Two-way bound value. |
options | { label, value }[] | — | Option list. |
optionClass | string | — | Per-option classes. |
Dialog
Modal dialog (focus-trapped).
Dialog installation
bun zt flow:add dialog
Or import directly from the package: import { Dialog } from "@zerotal/flow-ui";
Dialog preview
Dialog usage
<Button onClick={() => (this.open = true)}>Edit profile</Button>
<Dialog show={this.open} title="Edit profile" description="Make changes here.">
<form onSubmit={this.save} class="flex flex-col gap-3">
<Field label="Name"><Input value={this.form.name} /></Field>
<Button type="submit">Save</Button>
</form>
</Dialog>
Dialog props
| Prop | Type | Default | Description |
|---|---|---|---|
show | @expose boolean | — | Visibility (focus-trapped while open). |
title | node | — | Dialog title (wires aria-labelledby). |
description | node | — | Supporting text (aria-describedby). |
closable | boolean | true | Show the × + allow backdrop/Escape close. |
Sheet
Edge-anchored slide-over panel.
Sheet installation
bun zt flow:add sheet
Or import directly from the package: import { Sheet } from "@zerotal/flow-ui";
Sheet preview
Sheet usage
<Button onClick={() => (this.open = true)}>Open</Button>
<Sheet show={this.open} side="right" title="Edit profile">…</Sheet>
Sheet props
| Prop | Type | Default | Description |
|---|---|---|---|
show | @expose boolean | — | Visibility (focus-trapped while open). |
side | "left" | "right" | "top" | "bottom" | "right" | Edge to slide from. |
title | node | — | Header title. |
DropdownMenu
Keyboard-navigable menu (+ item/label/separator).
DropdownMenu installation
bun zt flow:add dropdown-menu
Or import directly from the package: import { DropdownMenu } from "@zerotal/flow-ui";
DropdownMenu preview
DropdownMenu usage
<DropdownMenu label="Options">
<DropdownMenuLabel>My account</DropdownMenuLabel>
<DropdownMenuItem onClick={this.profile}>Profile</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={this.signOut}>
Sign out
</DropdownMenuItem>
</DropdownMenu>
DropdownMenu props
| Prop | Type | Default | Description |
|---|---|---|---|
label | node | — | Default trigger label (or pass `trigger`). |
align | "left" | "right" | "left" | Panel alignment. |
Tabs
Tabbed panels with a pill tablist.
Tabs installation
bun zt flow:add tabs
Or import directly from the package: import { Tabs } from "@zerotal/flow-ui";
Tabs preview
Account settings.
Change your password.
Tabs usage
<Tabs
items={[
{ label: "Account", content: <AccountForm /> },
{ label: "Password", content: <PasswordForm /> },
]}
/>
Tabs props
| Prop | Type | Default | Description |
|---|---|---|---|
items | { label, content, name? }[] | — | Tabs + their panels. |
class | string | — | Extra classes. |
Alert
Inline alert (+ title/description).
Alert installation
bun zt flow:add alert
Or import directly from the package: import { Alert } from "@zerotal/flow-ui";
Alert preview
Alert usage
<Alert title="Heads up!">You can add components to your app.</Alert>
<Alert variant="destructive" title="Error">Something went wrong.</Alert>
Alert props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "default" | Visual style + ARIA role. |
title | node | — | Bold title line. |
dismissible | boolean | false | Show a client-only dismiss button. |
Tooltip
Hover/focus tooltip.
Tooltip installation
bun zt flow:add tooltip
Or import directly from the package: import { Tooltip } from "@zerotal/flow-ui";
Tooltip preview
Tooltip usage
<Tooltip content="Add to library">
<Button size="icon">+</Button>
</Tooltip>
Tooltip props
| Prop | Type | Default | Description |
|---|---|---|---|
content | node | — | Tooltip text. |
placement | "top" | "bottom" | "top" | Bubble position. |
Table
URL-sortable data table.
Table installation
bun zt flow:add table
Or import directly from the package: import { Table } from "@zerotal/flow-ui";
Table preview
| Name | Role |
|---|---|
| Ada Lovelace | Engineer |
| Alan Turing | Researcher |
Table usage
<Table
columns={[
{ key: "name", label: "Name", sortable: true },
{ key: "role", label: "Role" },
]}
rows={people}
sortBy={this.sortBy}
sortDir={this.sortDir}
hover
/>
Table props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | TableColumn[] | — | Column defs (key, label, sortable?, render?). |
rows | T[] | — | Row data. |
sortBy / sortDir | @url state | — | Bind to URL sort state for sortable headers. |
Popover
Floating panel anchored to a trigger.
Popover installation
bun zt flow:add popover
Or import directly from the package: import { Popover } from "@zerotal/flow-ui";
Popover preview
Anything at all.
Popover usage
<Popover trigger={<Button variant="outline">Options</Button>}>
<p class="text-sm">Anything at all.</p>
</Popover>
Popover props
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | node | — | The element that opens the panel. |
side | "top" | "right" | "bottom" | "left" | "bottom" | Which edge the panel sits on. |
align | "start" | "center" | "end" | "start" | How the panel lines up along that edge. |
class | string | — | Extra classes, merged last (wins over defaults). |
HoverCard
Preview panel shown on hover.
HoverCard installation
bun zt flow:add hover-card
Or import directly from the package: import { HoverCard } from "@zerotal/flow-ui";
HoverCard preview
Ada Mokoena
Joined in 2024
HoverCard usage
<HoverCard trigger={<a href="/users/1">@ada</a>}>
<p class="text-sm font-medium">Ada Mokoena</p>
</HoverCard>
HoverCard props
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | node | — | What is hovered. |
openDelay | number | 300 | Milliseconds before it opens. |
closeDelay | number | 150 | Grace period so the pointer can reach the panel. |
class | string | — | Extra classes, merged last (wins over defaults). |
AlertDialog
Confirm before something irreversible.
AlertDialog installation
bun zt flow:add alert-dialog
Or import directly from the package: import { AlertDialog } from "@zerotal/flow-ui";
AlertDialog preview
Delete this product?
It will be removed from every order. This cannot be undone.
AlertDialog usage
<AlertDialog
show={this.confirming}
title="Delete this product?"
description="It will be removed from every order. This cannot be undone."
confirmLabel="Delete"
onConfirm={this.destroy}
/>
AlertDialog props
| Prop | Type | Default | Description |
|---|---|---|---|
show | boolean | — | Bound @expose boolean controlling visibility. |
title | node | — | The question. |
description | node | — | What will happen. Worth a full sentence. |
onConfirm | handler | — | Server action for the confirming choice. |
destructive | boolean | true | Style the confirm button as destructive. |
Command
Searchable command menu (⌘K palette).
Command installation
bun zt flow:add command
Or import directly from the package: import { Command } from "@zerotal/flow-ui";
Command preview
Mounted hidden; press Ctrl K to open it.
Nothing found.
Command usage
<Command
items={[
{ label: "Products", href: "/admin/products", group: "Go to" },
{ label: "New order", href: "/admin/orders/create", group: "Create" },
]}
/>
Command props
| Prop | Type | Default | Description |
|---|---|---|---|
items | CommandItem[] | — | Destinations and actions, each with an optional group. |
hotkey | string | null | "k" | Key that opens it with the platform modifier. |
placeholder | string | "Search…" | Placeholder in the search box. |
emptyMessage | string | "Nothing found." | Shown when nothing matches. |
ContextMenu
Right-click menu.
ContextMenu installation
bun zt flow:add context-menu
Or import directly from the package: import { ContextMenu } from "@zerotal/flow-ui";
ContextMenu preview
ContextMenu usage
<ContextMenu
items={[
{ label: "Open", action: "$flow.open(id)" },
{ separator: true },
{ label: "Delete", action: "$flow.remove(id)", danger: true },
]}
>
<div>Right-click me</div>
</ContextMenu>
ContextMenu props
| Prop | Type | Default | Description |
|---|---|---|---|
items | ContextMenuItem[] | — | Entries, dividers and destructive actions. |
class | string | — | Extra classes, merged last (wins over defaults). |
Menubar
Application menu bar.
Menubar installation
bun zt flow:add menubar
Or import directly from the package: import { Menubar } from "@zerotal/flow-ui";
Menubar preview
Menubar usage
<Menubar
menus={[
{ label: "File", items: [{ label: "New", shortcut: "⌘N" }] },
{ label: "Edit", items: [{ label: "Undo", shortcut: "⌘Z" }] },
]}
/>
Menubar props
| Prop | Type | Default | Description |
|---|---|---|---|
menus | MenubarMenu[] | — | Top-level menus, each with its own items. |
class | string | — | Extra classes, merged last (wins over defaults). |
NavigationMenu
Site nav with dropdown panels.
NavigationMenu installation
bun zt flow:add navigation-menu
Or import directly from the package: import { NavigationMenu } from "@zerotal/flow-ui";
NavigationMenu preview
NavigationMenu usage
<NavigationMenu
items={[
{ label: "Docs", href: "/docs" },
{ label: "Products", panel: [{ label: "Admin", href: "/admin", description: "Back office" }] },
]}
/>
NavigationMenu props
| Prop | Type | Default | Description |
|---|---|---|---|
items | NavigationMenuItem[] | — | Links, some of which open a panel. |
class | string | — | Extra classes, merged last (wins over defaults). |
Sidebar
App shell nav rail with a mobile drawer.
Sidebar installation
bun zt flow:add sidebar
Or import directly from the package: import { Sidebar } from "@zerotal/flow-ui";
Sidebar preview
Sidebar usage
<Sidebar
brand="Zerotal"
tagline="Back office"
current={path}
groups={[{ label: "Shop", items: [{ label: "Products", href: "/admin/products", badge: 12 }] }]}
/>
Sidebar props
| Prop | Type | Default | Description |
|---|---|---|---|
groups | SidebarGroup[] | — | Nav tree, optionally nested one level. |
current | string | — | Current path, for marking the active item. |
collapsible | boolean | true | Render the mobile drawer toggle. |
footer | node | — | Pinned to the bottom — a user menu, a version. |
Breadcrumb
Trail showing where a page sits.
Breadcrumb installation
bun zt flow:add breadcrumb
Or import directly from the package: import { Breadcrumb } from "@zerotal/flow-ui";
Breadcrumb preview
Breadcrumb usage
<Breadcrumb
items={[
{ label: "Dashboard", href: "/admin" },
{ label: "Products", href: "/admin/products" },
{ label: "Desk Lamp" },
]}
/>
Breadcrumb props
| Prop | Type | Default | Description |
|---|---|---|---|
items | BreadcrumbItem[] | — | The trail. The last item renders as the current page. |
maxItems | number | — | Collapse a longer trail to first + last few. |
separator | node | — | What sits between items. |
Pagination
Page links with a windowed number range.
Pagination installation
bun zt flow:add pagination
Or import directly from the package: import { Pagination } from "@zerotal/flow-ui";
Pagination preview
Pagination usage
<Pagination
page={p.page}
lastPage={p.lastPage}
total={p.total}
perPage={p.perPage}
href={(n) => `?page=${n}`}
/>
Pagination props
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | — | Current page, 1-based. |
lastPage | number | — | How many pages there are. |
href | (page: number) => string | — | Builds each page's URL, keeping your other params. |
total | number | — | Row count, shown as “1–20 of 231”. |
siblings | number | 5 | Numbered links around the current page. |
Field
Label + control + description + error.
Field installation
bun zt flow:add field
Or import directly from the package: import { Field } from "@zerotal/flow-ui";
Field preview
We never share it.
Must be at least 8 characters.
Field usage
<Field label="Email" description="We never share it." error={errors.email} required>
<Input type="email" flow:model="form.email" />
</Field>
Field props
| Prop | Type | Default | Description |
|---|---|---|---|
label | node | — | Associated with the control by a generated id. |
description | node | — | Helper text, linked by aria-describedby. |
error | node | — | Its presence marks the field invalid and announces it. |
required | boolean | — | Shows the marker and sets aria-required. |
orientation | "vertical" | "horizontal" | "vertical" | Label above or beside. |
InputGroup
Input with affixes or addons.
InputGroup installation
bun zt flow:add input-group
Or import directly from the package: import { InputGroup } from "@zerotal/flow-ui";
InputGroup preview
InputGroup usage
<InputGroup prefix="R"><Input flow:model="form.price" /></InputGroup>
<InputGroup addonAfter={<Button>Copy</Button>}><Input value={key} /></InputGroup>
InputGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
prefix | node | — | Inside the border, before the text. |
suffix | node | — | Inside the border, after the text. |
addonBefore | node | — | Outside the border, in its own cell. |
addonAfter | node | — | Outside the border — often a button. |
InputOTP
One-time-code input.
InputOTP installation
bun zt flow:add input-otp
Or import directly from the package: import { InputOTP } from "@zerotal/flow-ui";
InputOTP preview
InputOTP usage
<InputOTP length={6} groupAfter={3} flow:model="form.code" />
InputOTP props
| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 6 | How many characters the code has. |
numeric | boolean | true | Restrict to digits. |
groupAfter | number | — | Insert a wider gap after this many boxes. |
Combobox
Autocomplete over many options.
Combobox installation
bun zt flow:add combobox
Or import directly from the package: import { Combobox } from "@zerotal/flow-ui";
Combobox preview
Combobox usage
<Combobox bind={this.brandId} options={brands} placeholder="Search brands…" />
Combobox props
| Prop | Type | Default | Description |
|---|---|---|---|
bind | @expose value | — | The chosen value. |
options | ComboboxOption[] | — | Choices, filtered as you type. |
query | @expose value | — | Bind to filter on the server instead of the client. |
Slider
Value chosen from a range.
Slider installation
bun zt flow:add slider
Or import directly from the package: import { Slider } from "@zerotal/flow-ui";
Slider preview
Slider usage
<Slider value={this.volume} max={100} showValue />
Slider props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Current value. |
min | number | 0 | Lower bound. |
max | number | 100 | Upper bound. |
showValue | boolean | — | Show the value beside the track. |
Toggle
Pressed-state button (+ toggle group).
Toggle installation
bun zt flow:add toggle
Or import directly from the package: import { Toggle } from "@zerotal/flow-ui";
Toggle preview
Toggle usage
<Toggle pressed={this.bold}>B</Toggle>
<ToggleGroup value={this.view} options={[
{ value: "list", label: "List" }, { value: "grid", label: "Grid" },
]} />
Toggle props
| Prop | Type | Default | Description |
|---|---|---|---|
pressed | boolean | — | Whether the toggle is on (sets aria-pressed). |
variant | "default" | "outline" | "default" | Visual style. |
options | ToggleOption[] | — | ToggleGroup only — the members. |
type | "single" | "multiple" | "single" | ToggleGroup only — how many may be on. |
ButtonGroup
Buttons joined into one control.
ButtonGroup installation
bun zt flow:add button-group
Or import directly from the package: import { ButtonGroup } from "@zerotal/flow-ui";
ButtonGroup preview
ButtonGroup usage
<ButtonGroup>
<Button variant="outline">Day</Button>
<Button variant="outline">Week</Button>
</ButtonGroup>
ButtonGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Direction the members join in. |
class | string | — | Extra classes, merged last (wins over defaults). |
Calendar
Month grid for picking or laying out dates.
Calendar installation
bun zt flow:add calendar
Or import directly from the package: import { Calendar } from "@zerotal/flow-ui";
Calendar preview
July 2026
Calendar usage
<Calendar value={this.due} onSelect={this.pick} />
<Calendar month="2026-07" events={[{ date: "2026-07-14", label: "Launch" }]} />
Calendar props
| Prop | Type | Default | Description |
|---|---|---|---|
month | string | — | `YYYY-MM` to display. |
value | string | — | Selected `YYYY-MM-DD`. |
onSelect | handler | — | Receives the clicked `YYYY-MM-DD`. |
events | CalendarEvent[] | — | Records to lay out across the month. |
min / max | string | — | Selectable range. |
DatePicker
Calendar in a popover.
DatePicker installation
bun zt flow:add date-picker
Or import directly from the package: import { DatePicker } from "@zerotal/flow-ui";
DatePicker preview
July 2026
DatePicker usage
<DatePicker value={this.due} onSelect={this.setDue} />
DatePicker props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Selected `YYYY-MM-DD`. |
onSelect | handler | — | Receives the clicked `YYYY-MM-DD`. |
placeholder | string | "Pick a date" | Shown when nothing is chosen. |
min / max | string | — | Selectable range. |
Toaster
Host for transient flash messages.
Toaster installation
bun zt flow:add toast
Or import directly from the package: import { Toaster } from "@zerotal/flow-ui";
Toaster preview
Mounted once per layout; every page.flash() lands in it.
Toaster usage
<Toaster position="bottom-right" />;
// then anywhere on the server:
page.flash("Saved.", "success");
Toaster props
| Prop | Type | Default | Description |
|---|---|---|---|
position | "top-right" | "bottom-right" | … | "bottom-right" | Corner it stacks in. |
duration | number | 4000 | How long a toast stays, in ms. |
max | number | 4 | Most on screen at once. |
Progress
Determinate progress bar.
Progress installation
bun zt flow:add progress
Or import directly from the package: import { Progress } from "@zerotal/flow-ui";
Progress preview
Progress usage
<Progress value={imported} max={total} showValue />
<Progress /> {/* indeterminate */}
Progress props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Completed amount. Omit for the indeterminate bar. |
max | number | 100 | The total. |
showValue | boolean | — | Show the percentage beside the bar. |
label | string | — | Describes what is progressing, for screen readers. |
Spinner
Indeterminate loading indicator.
Spinner installation
bun zt flow:add spinner
Or import directly from the package: import { Spinner } from "@zerotal/flow-ui";
Spinner preview
Spinner usage
<Spinner />
<Button disabled><Spinner size="sm" /> Saving…</Button>
Spinner props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "default" | Sizing. |
label | string | null | "Loading" | Announced to screen readers. |
Empty
Empty-state block with an action.
Empty installation
bun zt flow:add empty
Or import directly from the package: import { Empty } from "@zerotal/flow-ui";
Empty preview
No orders yet
Orders appear here as customers place them.
Empty usage
<Empty
icon={icon}
title="No orders yet"
description="Orders appear here as customers place them."
action={<Button>New order</Button>}
/>
Empty props
| Prop | Type | Default | Description |
|---|---|---|---|
title | node | — | What is missing. |
description | node | — | Why, or what to do about it. |
action | node | — | The next step — usually a button. |
bare | boolean | — | Drop the dashed border, inside a card that draws its own. |
Kbd
Keyboard key (+ platform modifier).
Kbd installation
bun zt flow:add kbd
Or import directly from the package: import { Kbd } from "@zerotal/flow-ui";
Kbd preview
Kbd usage
<KbdMod /> <Kbd>K</Kbd>
Kbd props
| Prop | Type | Default | Description |
|---|---|---|---|
children | node | — | The key. `<KbdMod />` renders ⌘ or Ctrl per platform. |
class | string | — | Extra classes, merged last (wins over defaults). |
Accordion
Stacked collapsible sections.
Accordion installation
bun zt flow:add accordion
Or import directly from the package: import { Accordion } from "@zerotal/flow-ui";
Accordion preview
Ships in 2–3 days.
30 days, no questions.
Accordion usage
<Accordion
items={[
{ label: "Shipping", content: <p>Ships in 2–3 days.</p> },
{ label: "Returns", content: <p>30 days, no questions.</p> },
]}
/>
Accordion props
| Prop | Type | Default | Description |
|---|---|---|---|
items | AccordionItem[] | — | Each with a label and its content. |
multiple | boolean | — | Allow several open at once. |
defaultIndex | number | -1 | Which starts open. |
Collapsible
One section that opens and closes.
Collapsible installation
bun zt flow:add collapsible
Or import directly from the package: import { Collapsible } from "@zerotal/flow-ui";
Collapsible preview
Rarely-changed settings live here.
Collapsible usage
<Collapsible label="Advanced">
<Field label="Timeout">
<Input />
</Field>
</Collapsible>
Collapsible props
| Prop | Type | Default | Description |
|---|---|---|---|
label | node | — | Text for the default trigger. |
trigger | node | — | A custom trigger instead. |
defaultOpen | boolean | — | Start open. |
ScrollArea
Scrollable region with a styled bar.
ScrollArea installation
bun zt flow:add scroll-area
Or import directly from the package: import { ScrollArea } from "@zerotal/flow-ui";
ScrollArea preview
ScrollArea usage
<ScrollArea class="h-72">…long list…</ScrollArea>
ScrollArea props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "both" | "vertical" | Which way it scrolls. |
fade | boolean | — | Fade the content at the scrollable edges. |
class | string | — | Extra classes, merged last (wins over defaults). |
Resizable
Two panes with a draggable handle.
Resizable installation
bun zt flow:add resizable
Or import directly from the package: import { Resizable } from "@zerotal/flow-ui";
Resizable preview
Resizable usage
<Resizable start={<Tree />} end={<Editor />} defaultSize={30} />
Resizable props
| Prop | Type | Default | Description |
|---|---|---|---|
start / end | node | — | The two panes. |
defaultSize | number | 50 | First pane's starting size, as a percentage. |
min | number | 10 | Smallest either pane may become. |
orientation | "horizontal" | "vertical" | "horizontal" | Split direction. |
Carousel
Snap-scrolling strip with controls.
Carousel installation
bun zt flow:add carousel
Or import directly from the package: import { Carousel } from "@zerotal/flow-ui";
Carousel preview
Carousel usage
<Carousel
items={products.map((p) => (
<ProductCard product={p} />
))}
/>
Carousel props
| Prop | Type | Default | Description |
|---|---|---|---|
items | node[] | — | The slides. |
itemClass | string | "w-64 sm:w-72" | Width of each slide. |
hideControls | boolean | — | Swipe and scroll only. |
AspectRatio
Fixed width-to-height box.
AspectRatio installation
bun zt flow:add aspect-ratio
Or import directly from the package: import { AspectRatio } from "@zerotal/flow-ui";
AspectRatio preview
AspectRatio usage
<AspectRatio ratio={16 / 9}>
<img src={cover} class="h-full w-full object-cover" />
</AspectRatio>
AspectRatio props
| Prop | Type | Default | Description |
|---|---|---|---|
ratio | number | 1 | Width ÷ height. |
class | string | — | Extra classes, merged last (wins over defaults). |
Item
Icon + title + description + action row.
Item installation
bun zt flow:add item
Or import directly from the package: import { Item } from "@zerotal/flow-ui";
Item preview
Item usage
<Item title="Team" description="4 members" action={<Button size="sm">Manage</Button>} />
Item props
| Prop | Type | Default | Description |
|---|---|---|---|
title | node | — | The primary line. |
description | node | — | The supporting line. |
action | node | — | Trailing content. |
href | string | — | Makes the whole row a link. |
Chart
SVG line, area, bar and donut charts.
Chart installation
bun zt flow:add chart
Or import directly from the package: import { Chart } from "@zerotal/flow-ui";
Chart preview
Chart usage
<Chart type="line" labels={days} datasets={[{ label: "Orders", data: counts }]} />
<Chart type="donut" labels={["Paid","Pending"]} datasets={[{ data: [82, 18] }]} />
Chart props
| Prop | Type | Default | Description |
|---|---|---|---|
type | "line" | "area" | "bar" | "donut" | "line" | Chart kind. |
labels | string[] | — | Axis or legend labels. |
datasets | ChartDataset[] | — | One or more series. |
height | number | 220 | Drawing height; width is fluid. |
format | (n: number) => string | — | Formats axis values and the accessible summary. |
Prose
Prose wrapper + heading helpers.
Prose installation
bun zt flow:add typography
Or import directly from the package: import { Prose } from "@zerotal/flow-ui";
Prose preview
A rendered document
Prose styles its descendants, for content that arrives as a blob — Markdown, a CMS field, a rich-text column.
- Headings, lists and quotes
- Inline
codeand code blocks
Prose usage
<Prose dangerouslySetInnerHTML={{ __html: rendered }} />
<H1>Page title</H1>
<Muted>Last updated yesterday</Muted>
Prose props
| Prop | Type | Default | Description |
|---|---|---|---|
children | node | — | Markup you did not author — Prose styles its descendants. |
class | string | — | Extra classes, merged last (wins over defaults). |
Testing
Set your suite up once as described in Testing. A flow-ui
component is a plain function returning a node, so it tests like any other
view — render it and assert on the string.
// tests/components/Button.test.ts
import { test, expect } from "bun:test";
import { Button } from "@zerotal/flow-ui";
test("renders the variant's classes", () => {
const html = String(Button({ variant: "destructive", children: "Delete" }));
expect(html).toContain("Delete");
expect(html).toContain("destructive");
});
Test your own components, not the library's. The twenty components ship with
their own suite; a test asserting that Button renders a <button> re-tests
someone else's work and breaks when they restyle. What earns a test is the
component you composed from them, and the props you pass it.
Copy-in components are yours the moment you run flow:add. Once the source
lives in your repo, it is application code — it changes when you edit it, and
nothing upstream will catch a regression you introduce:
// tests/components/StatusBadge.test.ts
import { test, expect } from "bun:test";
import { StatusBadge } from "../../resources/components/StatusBadge.tsx";
test("an overdue invoice is flagged", () => {
const html = String(StatusBadge({ status: "overdue" }));
expect(html).toContain("Overdue");
expect(html).toContain("bg-red"); // whatever your theme maps it to
});
Assert behaviour, not class strings, wherever you can. A test pinned to
bg-red-500 fails on a palette change that broke nothing. Prefer the visible
text, an aria- attribute, or a data- hook you control.
Note — Interactive behaviour — a dropdown opening, a dialog trapping focus — needs a real browser. See Browser Tests; rendering assertions stop at the markup the server produced.
References
The full export surface of @zerotal/flow-ui:
| Export | Kind | Description |
|---|---|---|
FlowUiProvider | provider | Registers the flow:list / flow:add / flow:init commands. |
COMPONENTS, UTILS, findComponent | registry | The manifest behind flow:add. |
cn(...classes) | util | Merge class strings (clsx + tailwind-merge). |
gva(base, config) | util | Build token-backed variant class functions. |
Button, Badge, Card (+ parts), Input, Textarea, Label | components | Styled leaf and composite components. |
Separator, Skeleton, Avatar | components | Styled leaf components. |
Switch, Checkbox, Select, RadioGroup | components | Themed wrappers over Flow's headless primitives. |
Dialog, Sheet, DropdownMenu (+ parts), Tabs, Alert, Tooltip, Table | components | Themed interactive components. |
Disclosure, Accordion, Popover, Listbox, Combobox, Field, Fieldset, Legend, Description | re-exports | Headless Flow primitives, re-exported so flow-ui stays a single import. |
CLI commands (registered by FlowUiProvider):
| Command | Signature | Description |
|---|---|---|
flow:init | flow:init [--dir <path>] [--css <path>] | Set up the shared utils and theme import. |
flow:add | flow:add <name[,name]> [--all] [--force] [--dir <path>] | Copy component source into your app. |
flow:list | flow:list | List every component available to add. |