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

Noteflow: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-1chart-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.
onClickhandlerServer action or client expression (standard Flow).
classstringExtra 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.
classstringExtra 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
classstringExtra classes on the surface.
childrennodeCompose 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 stateTwo-way bind to an @expose / form field (emits flow:model).
typestring"text"Native input type.
classstringExtra 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 stateTwo-way bind to an @expose / form field.
classstringExtra 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
forstringAssociated control id.
classstringExtra 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
classstringSize + 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 | nullImage URL; falls back to `fallback` when absent.
fallbacknodeShown when there's no image (e.g. initials).
altstringImage 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 booleanTwo-way bound boolean (server-synced).
classstringExtra 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 booleanTwo-way bound boolean.
classstringExtra 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 valueTwo-way bound value (flow:model).
options{ label, value }[]Option list.
placeholderstringOptional 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 valueTwo-way bound value.
options{ label, value }[]Option list.
optionClassstringPer-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 booleanVisibility (focus-trapped while open).
titlenodeDialog title (wires aria-labelledby).
descriptionnodeSupporting 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 booleanVisibility (focus-trapped while open).
side"left" | "right" | "top" | "bottom""right"Edge to slide from.
titlenodeHeader 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
labelnodeDefault 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.
classstringExtra 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.
titlenodeBold 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
contentnodeTooltip 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 stateBind 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
triggernodeThe 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.
classstringExtra 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
triggernodeWhat is hovered.
openDelaynumber300Milliseconds before it opens.
closeDelaynumber150Grace period so the pointer can reach the panel.
classstringExtra 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
showbooleanBound @expose boolean controlling visibility.
titlenodeThe question.
descriptionnodeWhat will happen. Worth a full sentence.
onConfirmhandlerServer 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.
classstringExtra 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.
classstringExtra 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.
classstringExtra 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.
currentstringCurrent path, for marking the active item.
collapsiblebooleantrueRender the mobile drawer toggle.
footernodePinned 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.
maxItemsnumberCollapse a longer trail to first + last few.
separatornodeWhat 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
pagenumberCurrent page, 1-based.
lastPagenumberHow many pages there are.
href(page: number) => stringBuilds each page's URL, keeping your other params.
totalnumberRow 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
labelnodeAssociated with the control by a generated id.
descriptionnodeHelper text, linked by aria-describedby.
errornodeIts presence marks the field invalid and announces it.
requiredbooleanShows 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
prefixnodeInside the border, before the text.
suffixnodeInside the border, after the text.
addonBeforenodeOutside the border, in its own cell.
addonAfternodeOutside 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.
groupAfternumberInsert 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 valueThe chosen value.
optionsComboboxOption[]Choices, filtered as you type.
query@expose valueBind 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
valuenumberCurrent value.
minnumber0Lower bound.
maxnumber100Upper bound.
showValuebooleanShow 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
pressedbooleanWhether 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.
classstringExtra 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.
valuestringSelected `YYYY-MM-DD`.
onSelecthandlerReceives the clicked `YYYY-MM-DD`.
eventsCalendarEvent[]Records to lay out across the month.
min / maxstringSelectable 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
valuestringSelected `YYYY-MM-DD`.
onSelecthandlerReceives the clicked `YYYY-MM-DD`.
placeholderstring"Pick a date"Shown when nothing is chosen.
min / maxstringSelectable 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
valuenumberCompleted amount. Omit for the indeterminate bar.
maxnumber100The total.
showValuebooleanShow the percentage beside the bar.
labelstringDescribes 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
titlenodeWhat is missing.
descriptionnodeWhy, or what to do about it.
actionnodeThe next step — usually a button.
barebooleanDrop 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
childrennodeThe key. `<KbdMod />` renders ⌘ or Ctrl per platform.
classstringExtra 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.
multiplebooleanAllow 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
labelnodeText for the default trigger.
triggernodeA custom trigger instead.
defaultOpenbooleanStart 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.
fadebooleanFade the content at the scrollable edges.
classstringExtra 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 / endnodeThe 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.
hideControlsbooleanSwipe 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.
classstringExtra 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
titlenodeThe primary line.
descriptionnodeThe supporting line.
actionnodeTrailing content.
hrefstringMakes 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) => stringFormats 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
childrennodeMarkup you did not author — Prose styles its descendants.
classstringExtra 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:

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.