Skip to content
tauler

Component reference

Every component tauler ships, grouped by kind. The components page says what the kinds mean and how to choose between them.

Module: @ui/badge

Shadcn reference: https://ui.shadcn.com/docs/components/badge

Badge screenshot

A small inline label for status, category, or count.

import { Badge } from "@ui/badge";
<div class="flex flex-row gap-[8px]">
<Badge><span>Default</span></Badge>
<Badge variant="secondary"><span>Secondary</span></Badge>
<Badge variant="destructive"><span>Destructive</span></Badge>
<Badge variant="outline"><span>Outline</span></Badge>
</div>

Module: @ui/card

Shadcn reference: https://ui.shadcn.com/docs/components/card

Card screenshot

A styled container with rounded corners, a border, and card background colour. Wraps arbitrary child nodes and accepts an optional class attribute for Tailwind overrides.

  • <CardContent>
  • <CardDescription>
  • <CardFooter>
  • <CardHeader>
  • <CardTitle>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
<Card class="flex flex-col gap-[6px]">
<CardHeader>
<CardTitle><span>System Status</span></CardTitle>
<CardDescription><span>All services operational</span></CardDescription>
</CardHeader>
<CardContent>
<span class="text-foreground text-[12px]">nginx · postgres · redis</span>
</CardContent>
</Card>

Module: @ui/datatable

Shadcn reference: https://ui.shadcn.com/docs/components/table

DataTable screenshot

A data-driven table. Renders a header row followed by data rows with alternating bg-card / bg-muted/30 backgrounds. Columns map a key (used to look up values in each row object) to a label (shown in the header). An optional width constrains the column.

For full compositional control, use the Table, TableHeader, TableBody, TableRow, TableHead, and TableCell primitives from @ui/table instead.

import { DataTable } from "@ui/datatable";
<DataTable
columns={[{key:"service", label:"SERVICE"}, {key:"status", label:"STATUS"}, {key:"uptime", label:"UPTIME"}]}
rows={[
{service:"nginx", status:"running", uptime:"14d"},
{service:"postgres", status:"running", uptime:"7d"},
{service:"redis", status:"stopped", uptime:"—"},
]}
/>

Module: @ui/icon

Icon screenshot

Renders a single Nerd Font glyph by icon name.

name uses the Nerd Fonts naming convention: {family}-{icon}, e.g. md-home, fa-github, cod-terminal. Full catalogue: https://www.nerdfonts.com/cheat-sheet

Unknown names render as ?.

import { Icon } from "@ui/icon";
<div class="flex flex-col gap-[16px] p-[12px]">
<div class="flex flex-row items-end gap-[20px]">
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-star" class="text-[12px]" />
<span class="text-[9px] text-muted-foreground">12px</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-star" class="text-[16px]" />
<span class="text-[9px] text-muted-foreground">16px</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-star" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">20px</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-star" class="text-[28px]" />
<span class="text-[9px] text-muted-foreground">28px</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-star" class="text-[36px]" />
<span class="text-[9px] text-muted-foreground">36px</span>
</div>
</div>
<div class="flex flex-row flex-wrap gap-x-[20px] gap-y-[12px]">
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-home" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">md-home</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-heart" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">md-heart</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="fa-github" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">fa-github</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="cod-terminal" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">cod-terminal</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="oct-git_branch" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">oct-git_branch</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="dev-linux" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">dev-linux</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-folder" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">md-folder</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="fa-star" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">fa-star</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="oct-repo" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">oct-repo</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="cod-search" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">cod-search</span>
</div>
<div class="flex flex-col items-center gap-[4px]">
<Icon name="md-wifi" class="text-[20px]" />
<span class="text-[9px] text-muted-foreground">md-wifi</span>
</div>
</div>
</div>

Module: @ui/progress

Shadcn reference: https://ui.shadcn.com/docs/components/progress

Progress screenshot

A horizontal progress bar. Renders a muted track with a filled segment proportional to value (0–100). An optional color prop overrides the fill colour; class applies extra Tailwind classes to the track.

import { Progress } from "@ui/progress";
<div class="flex flex-col gap-[6px] w-[200px]">
<div class="flex flex-row justify-between">
<span class="text-muted-foreground text-[11px]">Memory</span>
<span class="text-foreground text-[11px]">72%</span>
</div>
<Progress value={72} />
</div>

Module: @ui/table

Shadcn reference: https://ui.shadcn.com/docs/components/table

Table screenshot

Composable table primitives. Use these to build fully custom table layouts. For a data-driven table, use DataTable from @ui/datatable instead.

  • <TableBody>
  • <TableCell>
  • <TableHead>
  • <TableHeader>
  • <TableRow>
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ui/table";
<Table>
<TableHeader>
<TableRow>
<TableHead><span>SERVICE</span></TableHead>
<TableHead><span>STATUS</span></TableHead>
<TableHead><span>UPTIME</span></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell><span>nginx</span></TableCell>
<TableCell class="text-green-500"><span>running</span></TableCell>
<TableCell><span>14d</span></TableCell>
</TableRow>
</TableBody>
</Table>

Module: @ui/knob

Knob screenshot

A rotary knob. Draws value as an angle in degrees — 0 points up, and the angle increases clockwise — and reports the angle you turn it to.

It never remembers anything. value is read every tick from whatever owns it, and on_change receives the new angle and returns the intents to send — one, or an array of them.

<Module bin="~/.cargo/bin/tauler-audio">
{(data, events) => (
<Knob
value={data?.balance ?? 0}
step={5}
on_change={deg => events.setBalance({ deg })}
/>
)}
</Module>

The turn sets nothing locally: it sends intents, the module changes the angle, and the next tick brings the new value back. Omit on_change and the knob still renders — it is simply not interactive.

There is no min and no max, because the knob measures how far you have turned it rather than where on a scale you are pointing. Pressing it anywhere is a turn of zero, so it never jumps to meet the pointer, and a fast flick and a slow drag that end in the same place give the same angle.

The inner third is a hub that reports nothing. A bearing taken there is meaningless — undefined at the exact centre, and swinging through tens of degrees per pixel around it — so a press that lands in the hub, or a drag that wanders into it, is ignored rather than allowed to leap. Turn it by the rim.

value and the reported angle have deliberately different domains. value is drawn as given, so 450 and -90 point where they say. What on_change reports is always wrapped into 0–360, so turning past the top comes round rather than running off and a module’s stored number cannot drift out to thousands. What it cannot report is how many whole turns you made — there is no scale for them to mean anything on.

step defaults to 1 and rounds the turn, not the angle it lands on. Rounding the angle would move a press that has not travelled at all, and would shift the grid a little further every lap for a step that does not divide a circle. Rounding is also what keeps a turn from sending a message per pixel: a motion that produces the intents just sent is skipped.

import { Knob } from "@ui/knob";
<div class="flex flex-row gap-[12px] items-center">
<Knob value={0} />
<Knob value={45} />
<Knob value={135} />
<Knob value={250} />
</div>

Module: @ui/scroll-area

Shadcn reference: https://ui.shadcn.com/docs/components/scroll-area

ScrollArea screenshot

A scrollable viewport: clips children to its box and translates them up by scroll_top, with a <ScrollBar> rendered alongside as the draggable indicator. content_height and viewport_height describe the content’s full height and the visible window into it — the same terms <ScrollBar> uses, since the two share clamp_scroll_top so their notions of the valid range can’t drift apart.

It never remembers anything (ADR 0012): scroll_top is read every tick from whatever owns it, and on_change (wired through to the composed scrollbar’s thumb) receives where a drag has moved it and returns the intents to send.

<Module bin="~/.cargo/bin/tauler-logs">
{(data, events) => (
<ScrollArea
scroll_top={data?.scroll_top ?? 0}
content_height={data?.lines?.length * 18 ?? 0}
viewport_height={120}
on_change={top => events.setScrollTop({ top })}
class="h-[120px] w-[240px]"
>
{(data?.lines ?? []).map(line => <span class="text-[11px]">{line}</span>)}
</ScrollArea>
)}
</Module>

The drag sets nothing locally: it sends intents, the module changes scroll_top, and the next tick brings the new value back — the same round trip <Slider> and <Knob> make. Omit on_change and <ScrollArea> still renders, clipped and translated to scroll_top, just not draggable.

show_scrollbar controls the composed scrollbar’s visibility: omitted, it shows only when content_height > viewport_height (nothing to scroll draws no visible thumb, matching shadcn’s own default); true forces it to always show, false forces it to always hide, regardless of whether scrolling is actually needed.

import { ScrollArea } from "@ui/scroll-area";
<ScrollArea
scroll_top={40}
content_height={300}
viewport_height={120}
class="h-[120px] w-[240px]"
>
<span class="text-[11px]">a lot of content…</span>
</ScrollArea>

Module: @ui/scroll-bar

Shadcn reference: https://ui.shadcn.com/docs/components/scroll-area

ScrollBar screenshot

A vertical scrollbar: a track with a thumb whose position and size are derived from scroll_top, content_height, and viewport_height — the thumb sits at scroll_top / content_height down the track and spans viewport_height / content_height of it, floored at MIN_THUMB_HEIGHT so it never shrinks below a draggable size.

<ScrollArea> renders one of these internally, so most layouts never reach for <ScrollBar> directly. Import it standalone when a scrollable region needs its indicator drawn somewhere other than where <ScrollArea> puts it — alongside the viewport rather than inside it, for instance.

It never remembers anything (ADR 0012): scroll_top is read every tick from whatever owns it, and on_change receives where a drag has moved the thumb to and returns the intents to send. Omit on_change and the scrollbar still renders — a static indicator of scroll_top, not interactive.

import { ScrollBar } from "@ui/scroll-bar";
<ScrollBar
scroll_top={40}
content_height={480}
viewport_height={120}
class="h-[120px]"
/>

Module: @ui/slider

Shadcn reference: https://ui.shadcn.com/docs/components/slider

Slider screenshot

A horizontal slider. Draws value within min–max, and reports where you press or drag.

It never remembers anything. value is read every tick from whatever owns it, and on_change receives the value under the pointer and returns the intents to send — one, or an array of them. Pressing counts as the first drag event, so a plain click sets the value too.

<Module bin="~/.cargo/bin/tauler-audio">
{(data, events) => (
<Slider
value={data?.volume ?? 0}
step={5}
on_change={v => events.setVolume({ volume: v })}
/>
)}
</Module>

The drag sets nothing locally: it sends intents, the module changes the volume, and the next tick brings the new value back. Omit on_change and the slider still renders — it is simply not interactive.

min defaults to 0, max to 100, and step to 1. step rounds the reported value, which is also what keeps a drag from sending a message per pixel: a motion that produces the intents just sent is skipped.

The example below names a module, tauler-demo-volume, that you will not have — pasted as it stands it renders a slider that does not move. It names one anyway, because a slider with a literal value and no source would show you a control holding its own state, which is the one thing this component does not do. On this page the module is a few lines of JavaScript in the browser rather than a subprocess; the layout file cannot tell the difference, and that is the point.

import { Slider } from "@ui/slider";
const events = useEvents("tauler-demo-volume");
const volume = Number(useStringStream("tauler-demo-volume")) || 40;
return (
<div class="flex flex-col gap-[6px] w-[200px]">
<div class="flex flex-row justify-between">
<span class="text-muted-foreground text-[11px]">Volume</span>
<span class="text-foreground text-[11px]">{volume}%</span>
</div>
<Slider value={volume} step={5} on_change={(v) => events.set({ value: v })} />
</div>
);