Skip to main content
zerotal

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:

GroupComponents
FormsField, Input, InputGroup, InputOTP, Textarea, Select, Combobox, Checkbox, RadioGroup, Switch, Slider, Toggle, ToggleGroup, Label, Calendar, DatePicker
Buttons & actionsButton, ButtonGroup, DropdownMenu, ContextMenu, Menubar
OverlaysDialog, AlertDialog, Sheet, Popover, HoverCard, Tooltip, Command
NavigationSidebar, NavigationMenu, Breadcrumb, Pagination, Tabs
DataTable, Chart, Item, Avatar, Badge
FeedbackToaster, Alert, Progress, Spinner, Skeleton, Empty
Layout & contentCard, 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 the flow:list, flow:add, and flow:init console commands (only when the commands binding 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:init is 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 than resources/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:

TokenFor
success / warningThe states destructive doesn't cover — "healthy" and "degraded".
flow-toast-success / flow-toast-warningStatus accents the toast host reads at runtime.
chart-1 … chart-7A 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 cn util needs clsx and tailwind-merge in your app. flow:add and flow:init warn you with the exact bun add command 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:add when 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-ui when 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

PropTypeDefaultDescription
variant"default" | "secondary" | "destructive" | "outline" | "ghost" | "link""default"Visual style.
size"default" | "sm" | "lg" | "icon""default"Sizing.
onClickhandler—Server action or client expression (standard Flow).
classstring—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

DefaultSecondaryDestructiveOutline

Badge usage

<Badge>New</Badge>
<Badge variant="secondary">Beta</Badge>
<Badge variant="destructive">Overdue</Badge>
<Badge variant="outline">Draft</Badge>

Badge props

PropTypeDefaultDescription
variant"default" | "secondary" | "destructive" | "outline""default"Visual style.
classstring—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

PropTypeDefaultDescription
classstring—Extra classes on the surface.
childrennode—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

PropTypeDefaultDescription
valuebound state—Two-way bind to an @expose / form field (emits flow:model).
typestring"text"Native input type.
classstring—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

PropTypeDefaultDescription
valuebound state—Two-way bind to an @expose / form field.
classstring—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

PropTypeDefaultDescription
forstring—Associated control id.
classstring—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.

Blog
Docs
Source

Separator usage

<Separator />
<Separator orientation="vertical" class="h-6" />

Separator props

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Divider direction.
decorativebooleantrueARIA-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

PropTypeDefaultDescription
classstring—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

Ada MokoenaALGH

Avatar usage

<Avatar src={user.avatarUrl} alt={user.name} fallback="AL" />
<Avatar fallback="GH" />

Avatar props

PropTypeDefaultDescription
srcstring | null—Image URL; falls back to `fallback` when absent.
fallbacknode—Shown when there's no image (e.g. initials).
altstring—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

PropTypeDefaultDescription
bind@expose boolean—Two-way bound boolean (server-synced).
classstring—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

PropTypeDefaultDescription
bind@expose boolean—Two-way bound boolean.
classstring—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

PropTypeDefaultDescription
bind@expose value—Two-way bound value (flow:model).
options{ label, value }[]—Option list.
placeholderstring—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

PropTypeDefaultDescription
bind@expose value—Two-way bound value.
options{ label, value }[]—Option list.
optionClassstring—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

PropTypeDefaultDescription
show@expose boolean—Visibility (focus-trapped while open).
titlenode—Dialog title (wires aria-labelledby).
descriptionnode—Supporting text (aria-describedby).
closablebooleantrueShow 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

PropTypeDefaultDescription
show@expose boolean—Visibility (focus-trapped while open).
side"left" | "right" | "top" | "bottom""right"Edge to slide from.
titlenode—Header title.

Keyboard-navigable menu (+ item/label/separator).

bun zt flow:add dropdown-menu

Or import directly from the package: import { DropdownMenu } from "@zerotal/flow-ui";

<DropdownMenu label="Options">
  <DropdownMenuLabel>My account</DropdownMenuLabel>
  <DropdownMenuItem onClick={this.profile}>Profile</DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="destructive" onClick={this.signOut}>
    Sign out
  </DropdownMenuItem>
</DropdownMenu>
PropTypeDefaultDescription
labelnode—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

PropTypeDefaultDescription
items{ label, content, name? }[]—Tabs + their panels.
classstring—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

Heads up!
You can add components to your app using the CLI.

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

PropTypeDefaultDescription
variant"default" | "destructive""default"Visual style + ARIA role.
titlenode—Bold title line.
dismissiblebooleanfalseShow 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

PropTypeDefaultDescription
contentnode—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

NameRole
Ada LovelaceEngineer
Alan TuringResearcher

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

PropTypeDefaultDescription
columnsTableColumn[]—Column defs (key, label, sortable?, render?).
rowsT[]—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

PropTypeDefaultDescription
triggernode—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.
classstring—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

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

PropTypeDefaultDescription
triggernode—What is hovered.
openDelaynumber300Milliseconds before it opens.
closeDelaynumber150Grace period so the pointer can reach the panel.
classstring—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

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

PropTypeDefaultDescription
showboolean—Bound @expose boolean controlling visibility.
titlenode—The question.
descriptionnode—What will happen. Worth a full sentence.
onConfirmhandler—Server action for the confirming choice.
destructivebooleantrueStyle 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.

Command usage

<Command
  items={[
    { label: "Products", href: "/admin/products", group: "Go to" },
    { label: "New order", href: "/admin/orders/create", group: "Create" },
  ]}
/>

Command props

PropTypeDefaultDescription
itemsCommandItem[]—Destinations and actions, each with an optional group.
hotkeystring | null"k"Key that opens it with the platform modifier.
placeholderstring"Search…"Placeholder in the search box.
emptyMessagestring"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

Right-click me

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

PropTypeDefaultDescription
itemsContextMenuItem[]—Entries, dividers and destructive actions.
classstring—Extra classes, merged last (wins over defaults).

Application menu bar.

bun zt flow:add menubar

Or import directly from the package: import { Menubar } from "@zerotal/flow-ui";

<Menubar
  menus={[
    { label: "File", items: [{ label: "New", shortcut: "⌘N" }] },
    { label: "Edit", items: [{ label: "Undo", shortcut: "⌘Z" }] },
  ]}
/>
PropTypeDefaultDescription
menusMenubarMenu[]—Top-level menus, each with its own items.
classstring—Extra classes, merged last (wins over defaults).

Site nav with dropdown panels.

bun zt flow:add navigation-menu

Or import directly from the package: import { NavigationMenu } from "@zerotal/flow-ui";

<NavigationMenu
  items={[
    { label: "Docs", href: "/docs" },
    { label: "Products", panel: [{ label: "Admin", href: "/admin", description: "Back office" }] },
  ]}
/>
PropTypeDefaultDescription
itemsNavigationMenuItem[]—Links, some of which open a panel.
classstring—Extra classes, merged last (wins over defaults).

App shell nav rail with a mobile drawer.

bun zt flow:add sidebar

Or import directly from the package: import { Sidebar } from "@zerotal/flow-ui";

<Sidebar
  brand="Zerotal"
  tagline="Back office"
  current={path}
  groups={[{ label: "Shop", items: [{ label: "Products", href: "/admin/products", badge: 12 }] }]}
/>
PropTypeDefaultDescription
groupsSidebarGroup[]—Nav tree, optionally nested one level.
currentstring—Current path, for marking the active item.
collapsiblebooleantrueRender the mobile drawer toggle.
footernode—Pinned to the bottom — a user menu, a version.

Trail showing where a page sits.

bun zt flow:add breadcrumb

Or import directly from the package: import { Breadcrumb } from "@zerotal/flow-ui";

<Breadcrumb
  items={[
    { label: "Dashboard", href: "/admin" },
    { label: "Products", href: "/admin/products" },
    { label: "Desk Lamp" },
  ]}
/>
PropTypeDefaultDescription
itemsBreadcrumbItem[]—The trail. The last item renders as the current page.
maxItemsnumber—Collapse a longer trail to first + last few.
separatornode—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

PropTypeDefaultDescription
pagenumber—Current page, 1-based.
lastPagenumber—How many pages there are.
href(page: number) => string—Builds each page's URL, keeping your other params.
totalnumber—Row count, shown as “1–20 of 231”.
siblingsnumber5Numbered 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.

Field usage

<Field label="Email" description="We never share it." error={errors.email} required>
  <Input type="email" flow:model="form.email" />
</Field>

Field props

PropTypeDefaultDescription
labelnode—Associated with the control by a generated id.
descriptionnode—Helper text, linked by aria-describedby.
errornode—Its presence marks the field invalid and announces it.
requiredboolean—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

R

InputGroup usage

<InputGroup prefix="R"><Input flow:model="form.price" /></InputGroup>
<InputGroup addonAfter={<Button>Copy</Button>}><Input value={key} /></InputGroup>

InputGroup props

PropTypeDefaultDescription
prefixnode—Inside the border, before the text.
suffixnode—Inside the border, after the text.
addonBeforenode—Outside the border, in its own cell.
addonAfternode—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

PropTypeDefaultDescription
lengthnumber6How many characters the code has.
numericbooleantrueRestrict to digits.
groupAfternumber—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

  • Acme
  • Globex
  • Initech

Combobox usage

<Combobox bind={this.brandId} options={brands} placeholder="Search brands…" />

Combobox props

PropTypeDefaultDescription
bind@expose value—The chosen value.
optionsComboboxOption[]—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

PropTypeDefaultDescription
valuenumber—Current value.
minnumber0Lower bound.
maxnumber100Upper bound.
showValueboolean—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

PropTypeDefaultDescription
pressedboolean—Whether the toggle is on (sets aria-pressed).
variant"default" | "outline""default"Visual style.
optionsToggleOption[]—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

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Direction the members join in.
classstring—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

Mon
Tue
Wed
Thu
Fri
Sat
Sun
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9

Calendar usage

<Calendar value={this.due} onSelect={this.pick} />
<Calendar month="2026-07" events={[{ date: "2026-07-14", label: "Launch" }]} />

Calendar props

PropTypeDefaultDescription
monthstring—`YYYY-MM` to display.
valuestring—Selected `YYYY-MM-DD`.
onSelecthandler—Receives the clicked `YYYY-MM-DD`.
eventsCalendarEvent[]—Records to lay out across the month.
min / maxstring—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

14 Jul 2026

July 2026

Mon
Tue
Wed
Thu
Fri
Sat
Sun
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9

DatePicker usage

<DatePicker value={this.due} onSelect={this.setDue} />

DatePicker props

PropTypeDefaultDescription
valuestring—Selected `YYYY-MM-DD`.
onSelecthandler—Receives the clicked `YYYY-MM-DD`.
placeholderstring"Pick a date"Shown when nothing is chosen.
min / maxstring—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

PropTypeDefaultDescription
position"top-right" | "bottom-right" | …"bottom-right"Corner it stacks in.
durationnumber4000How long a toast stays, in ms.
maxnumber4Most 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

62%

Progress usage

<Progress value={imported} max={total} showValue />
<Progress />  {/* indeterminate */}

Progress props

PropTypeDefaultDescription
valuenumber—Completed amount. Omit for the indeterminate bar.
maxnumber100The total.
showValueboolean—Show the percentage beside the bar.
labelstring—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

LoadingLoadingLoading

Spinner usage

<Spinner />
<Button disabled><Spinner size="sm" /> Saving…</Button>

Spinner props

PropTypeDefaultDescription
size"sm" | "default" | "lg""default"Sizing.
labelstring | 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

PropTypeDefaultDescription
titlenode—What is missing.
descriptionnode—Why, or what to do about it.
actionnode—The next step — usually a button.
bareboolean—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

CtrlK⇧↵

Kbd usage

<KbdMod /> <Kbd>K</Kbd>

Kbd props

PropTypeDefaultDescription
childrennode—The key. `<KbdMod />` renders ⌘ or Ctrl per platform.
classstring—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

PropTypeDefaultDescription
itemsAccordionItem[]—Each with a label and its content.
multipleboolean—Allow several open at once.
defaultIndexnumber-1Which 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

PropTypeDefaultDescription
labelnode—Text for the default trigger.
triggernode—A custom trigger instead.
defaultOpenboolean—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

Row 1

Row 2

Row 3

Row 4

Row 5

Row 6

Row 7

Row 8

Row 9

Row 10

Row 11

Row 12

ScrollArea usage

<ScrollArea class="h-72">…long list…</ScrollArea>

ScrollArea props

PropTypeDefaultDescription
orientation"vertical" | "horizontal" | "both""vertical"Which way it scrolls.
fadeboolean—Fade the content at the scrollable edges.
classstring—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

Sidebar
Content

Resizable usage

<Resizable start={<Tree />} end={<Editor />} defaultSize={30} />

Resizable props

PropTypeDefaultDescription
start / endnode—The two panes.
defaultSizenumber50First pane's starting size, as a percentage.
minnumber10Smallest either pane may become.
orientation"horizontal" | "vertical""horizontal"Split direction.

Snap-scrolling strip with controls.

bun zt flow:add carousel

Or import directly from the package: import { Carousel } from "@zerotal/flow-ui";

Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
Slide 6
<Carousel
  items={products.map((p) => (
    <ProductCard product={p} />
  ))}
/>
PropTypeDefaultDescription
itemsnode[]—The slides.
itemClassstring"w-64 sm:w-72"Width of each slide.
hideControlsboolean—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

16 / 9

AspectRatio usage

<AspectRatio ratio={16 / 9}>
  <img src={cover} class="h-full w-full object-cover" />
</AspectRatio>

AspectRatio props

PropTypeDefaultDescription
rationumber1Width ÷ height.
classstring—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

Team4 membersOwner
BillingVisa ending 4242

Item usage

<Item title="Team" description="4 members" action={<Button size="sm">Manage</Button>} />

Item props

PropTypeDefaultDescription
titlenode—The primary line.
descriptionnode—The supporting line.
actionnode—Trailing content.
hrefstring—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

013253850MonTueWedThuFriSatSun

Paid82Pending24Refunded6

Chart usage

<Chart type="line" labels={days} datasets={[{ label: "Orders", data: counts }]} />
<Chart type="donut" labels={["Paid","Pending"]} datasets={[{ data: [82, 18] }]} />

Chart props

PropTypeDefaultDescription
type"line" | "area" | "bar" | "donut""line"Chart kind.
labelsstring[]—Axis or legend labels.
datasetsChartDataset[]—One or more series.
heightnumber220Drawing 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 code and code blocks

Prose usage

<Prose dangerouslySetInnerHTML={{ __html: rendered }} />
<H1>Page title</H1>
<Muted>Last updated yesterday</Muted>

Prose props

PropTypeDefaultDescription
childrennode—Markup you did not author — Prose styles its descendants.
classstring—Extra classes, merged last (wins over defaults).

Types

Every component exports the type of its own props, named after it — Button takes ButtonProps, Dialog takes DialogProps. Reach for them when you wrap a component rather than use it directly, which is the usual reason an app needs one:

import { Button, type ButtonProps } from "@zerotal/flow-ui";

/** Our save button: the same API, one decision already made. */
export function SaveButton(props: Omit<ButtonProps, "variant">) {
  return <Button variant="primary" {...props} />;
}

The full set:

AccordionProps       AlertDialogProps     AlertProps           AlertTextProps
AspectRatioProps     AvatarProps          BadgeProps           BreadcrumbProps
ButtonGroupProps     ButtonProps          CalendarProps        CardElementProps
CarouselProps        ChartProps           CheckboxProps        CollapsibleProps
ComboboxProps        CommandProps         ContextMenuProps     DatePickerProps
DialogProps          DropdownMenuItemProps                     DropdownMenuLabelProps
DropdownMenuProps    DropdownMenuSeparatorProps                DropdownMenuShortcutProps
EmptyProps           FieldProps           GvaProps             HoverCardProps
InputGroupProps      InputOTPProps        InputProps           ItemProps
KbdProps             LabelProps           MenubarProps         NavigationMenuProps
PaginationProps      PopoverProps         ProgressProps        ProseProps
RadioGroupProps      ResizableProps       ScrollAreaProps      SelectProps
SeparatorProps       SheetProps           SidebarProps         SkeletonProps
SliderProps          SpinnerProps         SwitchProps          TableProps
TabsProps            TextProps            TextareaProps        ToasterProps
ToggleGroupProps     ToggleProps          TooltipProps

Values a prop takes

Several props are unions rather than free strings, and the union is exported so a variable holding one can be typed:

TypeUsed by
SelectOptionSelect, Combobox — one { label, value } entry.
RadioOptionRadioGroup.
TabItemTabs.
SidebarItemSidebar.
TableGroupTable — a grouped set of rows.
NavigationPanelLinkNavigationMenu.
PopoverSidePopover, Tooltip, HoverCard — which side it opens on.
PopoverAlignThe same, for alignment.
ToastPositionToaster — where toasts stack.
ChartTypeChart.
ClassValueAnything the class prop accepts, before merging.

Sub-components

A few components are used as a small set rather than one tag: AlertTitle and AlertDescription inside Alert, DropdownMenuShortcut inside a menu item. The prose components — Blockquote, Lead, H2, H3, H4 — are the same idea for Prose.

Variants and theming

Each variant-bearing component exports its own cva config, so a wrapper can reuse the exact class set rather than re-deriving it: buttonVariants, badgeVariants, alertVariants, spinnerVariants, toggleVariants, and popoverSurface for the floating surfaces. GvaConfig is the shape they are built from.

Theming is flowTokensCss (the CSS custom properties), flowTailwindConfig (the Tailwind preset that reads them), and FlowUiThemeConfig for config/flow-ui.ts. THEME, THEME_STORAGE_KEY and THEME_TOGGLE_SCRIPT are the light/dark toggle: the script runs before first paint, so a page does not flash the wrong theme on load.

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:

ExportKindDescription
FlowUiProviderproviderRegisters the flow:list / flow:add / flow:init commands.
COMPONENTS, UTILS, findComponentregistryThe manifest behind flow:add.
cn(...classes)utilMerge class strings (clsx + tailwind-merge).
gva(base, config)utilBuild token-backed variant class functions.
Button, Badge, Card (+ parts), Input, Textarea, LabelcomponentsStyled leaf and composite components.
Separator, Skeleton, AvatarcomponentsStyled leaf components.
Switch, Checkbox, Select, RadioGroupcomponentsThemed wrappers over Flow's headless primitives.
Dialog, Sheet, DropdownMenu (+ parts), Tabs, Alert, Tooltip, TablecomponentsThemed interactive components.
Disclosure, Accordion, Popover, Listbox, Combobox, Field, Fieldset, Legend, Descriptionre-exportsHeadless Flow primitives, re-exported so flow-ui stays a single import.

CLI commands (registered by FlowUiProvider):

CommandSignatureDescription
flow:initflow:init [--dir <path>] [--css <path>]Set up the shared utils and theme import.
flow:addflow:add <name[,name]> [--all] [--force] [--dir <path>]Copy component source into your app.
flow:listflow:listList every component available to add.

Next steps

  • Flow — the server-driven component framework these wrap.
  • Validator — validate the form fields you bind with Input and Select.
  • Assets — build the Tailwind CSS that powers the theme tokens.