Components

Props, types, and one quick example per component.

import {
  Page, Section, Stack, Grid, Card, Toolbar, Fragment,
  Text, Stat, Divider, Badge, EmptyState, Table,
  Button, SubmitButton, ResetButton, Link,
  Form, Field, HiddenInput, NumberInput, DateInput, DateTimeInput,
  TextInput, Textarea, Select, Checkbox, Toggle,
} from "@backfill-io/sdk/ui";

Props are validated server-side. Passing an unknown prop, the wrong type, or omitting a required prop throws at render time.

Layout

<Page>

Top-level container for every render script. Renders title, optional description, optional host-header toolbar, and a children slot.

PropTypeNotes
titlestringRequired. Browser title and page header.
descriptionstringSubtitle below the page header.
toolbarnodeSlot for a <Toolbar> of header actions.
childrennodePage body.
<Page title="Reconciliation" description="Match Stripe charges to invoices.">
  <Section>...</Section>
</Page>

<Section>

Visual grouping with optional title and description. Use multiple sections to break a page into named regions.

PropTypeNotes
titlestringOptional section heading.
descriptionstringSubtitle below the heading.
childrennodeSection body.
<Section title="Open invoices" description="Last 30 days.">
  <Table ... />
</Section>

<Stack>

Flex container. Use for stacking children with consistent gaps.

PropTypeDefault
direction"row" | "column""column"
gap"xs" | "sm" | "md" | "lg""md"
span1 - 12Optional grid column span on medium screens and up.
align"start" | "center" | "end" | "stretch"Optional cross-axis alignment.
justify"start" | "center" | "end" | "between"Optional main-axis distribution.
self"start" | "center" | "end" | "stretch"Optional alignment of this stack inside a parent grid.
wrapbooleanAllow row children to wrap.
childrennode
<Stack direction="row" gap="sm">
  <Badge tone="success">Live</Badge>
  <Text tone="muted">Synced 2 minutes ago</Text>
</Stack>

Use justify="end" and self="center" for row action groups in dense grids:

<Stack direction="row" span={3} justify="end" self="center" gap="xs">
  <Button action="bump-hours">+.25</Button>
  <Button action="delete-entry" variant="dangerLink">Delete</Button>
</Stack>

<Grid>

Fixed-column responsive grid.

PropTypeDefault
columns1 - 121
gap"xs" | "sm" | "md" | "lg""md"
childrennode
<Grid columns={3} gap="md">
  <Card><Stat label="Pending" value={pendingCount} /></Card>
  <Card><Stat label="Reconciled" value={doneCount} /></Card>
  <Card><Stat label="Errors" value={errorCount} /></Card>
</Grid>

<Card>

Bordered container with optional title.

PropTypeNotes
titlestringCard header.
hrefstringOptional same-origin destination; makes the card container clickable.
tone"default" | "subtle"Optional background/border treatment.
density"default" | "compact"Optional tighter padding.
childrennodeCard body.
<Card title="This month">
  <Stat label="Revenue" value="$12,400" />
</Card>

Use href when a repeated card should open a detail page. Nested buttons still invoke their own actions.

<Card href={Routes.page(ctx, "work-week-detail", { query: { id: week.id } })}>
  <Text>{week.customerName}</Text>
  <Button action="delete-week" variant="dangerLink" params={{ workWeekId: week.id }}>
    Delete Week
  </Button>
</Card>

Use tone="subtle" for lightweight form grouping without adding custom CSS:

<Card tone="subtle" density="compact">
  <Form action="save-line">...</Form>
</Card>

<Toolbar>

Container for action buttons, typically passed to <Page toolbar={...}>.

PropType
childrennode
<Page
  title="Vendors"
  toolbar={<Toolbar><Link href="/vendors/new">New vendor</Link></Toolbar>}
>
  ...
</Page>

<Fragment>

Render multiple children without a wrapping element. Returned implicitly when you write <>...</>.

<Fragment>
  <Text>First line</Text>
  <Text>Second line</Text>
</Fragment>

Display

<Text>

Plain text with optional tone.

PropTypeDefault
tone"default" | "muted""default"
childrennode
<Text tone="muted">No matches found.</Text>

<Stat>

Big-number display. Both label and value accept strings, numbers, or booleans.

PropTypeNotes
labelstring | number | booleanRequired.
valuestring | number | booleanRequired.
tone"default" | "good" | "bad" | "warning" | "neutral"Optional value color.
childrennodeOptional caption beneath the value.
<Stat label="Outstanding AR" value="$48,210" tone="warning">
  <Text tone="muted">across 12 invoices</Text>
</Stat>

<Divider>

Section separator with an optional centered label.

PropTypeNotes
labelstringOptional.
<Divider label="Open tasks" />

<Badge>

Small colored chip.

PropTypeDefault
tone"info" | "success" | "warning" | "error" | "neutral""neutral"
childrennode
<Badge tone="warning">Needs review</Badge>

<EmptyState>

Placeholder for empty result sets.

PropTypeNotes
titlestringRequired.
descriptionstringOptional supporting copy.
{rows.length === 0
  ? <EmptyState title="No invoices yet" description="They'll show up once Stripe sends the first webhook." />
  : <Table rows={rows} columns={cols} />}

<Table>

Tabular data. Pass an array of plain-object rows and a column descriptor.

PropTypeNotes
columnsTableColumn[]Required.
rowsRecord<string, unknown>[]Required.
type TableColumn = {
  key: string;                                // matches a key on each row
  label: string;
  align?: "start" | "center" | "end";
  cell?:
    | { kind: "badge"; tone?: BadgeTone }
    | { kind: "link"; hrefKey: string; labelKey?: string }
    | { kind: "action"; action: string; label?: string; labelKey?: string; params?: Record<string, string | number | boolean>; paramsKey?: string }
    | { kind: "money"; currency?: string; currencyKey?: string }
    | { kind: "date" };
};
import { Customer } from "@backfill-io/sdk";

<Table
  rows={Customer.query({ limit: 50 })}
  columns={[
    { key: "name",   label: "Customer" },
    { key: "status", label: "Status", cell: { kind: "badge" } },
    { key: "balance", label: "Balance", align: "end", cell: { kind: "money", currency: "USD" } },
    { key: "href", label: "Open", cell: { kind: "link", hrefKey: "href", labelKey: "name" } },
    { key: "deleteLabel", label: "Actions", cell: { kind: "action", action: "delete-customer", params: { customerId: "id" } } },
  ]}
/>

Interaction

<Button>

Triggers a UI action declared in ui.actions[]. The action prop is the action’s id.

PropTypeNotes
actionstringRequired. The action id.
variant"button" | "link" | "dangerLink"Optional visual treatment. Defaults to "button".
paramsRecord<string, string | number | boolean>Optional action-specific params, exposed as ctx.action.params.
childrennodeButton label.
<Button action="delete-entry" variant="dangerLink" params={{ entryId: entry.id }}>Delete</Button>

For navigation, use <Link>. <Button> is for invoking server-side action handlers.

<SubmitButton>

Submits the surrounding <Form>. Optionally invokes a different action than the form’s default.

PropTypeNotes
actionstringOptional override of the form’s action id.
paramsRecord<string, string | number | boolean>Optional action-specific params, exposed as ctx.action.params.
childrennodeButton label.
<Form action="save-customer">
  <Field name="name" label="Name"><TextInput name="name" /></Field>
  <SubmitButton params={{ customerId: existing.id }}>Save</SubmitButton>
</Form>

<ResetButton>

Resets the surrounding form to its initial values. No props beyond children.

<ResetButton>Reset</ResetButton>

Navigation link to another URL — extension page, dashboard route, or external.

PropTypeNotes
hrefstringRequired.
variant"link" | "button" | "primary"Optional visual treatment.
mode"link" | "navigate" | "patch"Optional navigation behavior. Defaults to plain browser navigation.
childrennodeLink text.
<Link href="/nexus-addresses">Back to nexus addresses</Link>

Use mode="patch" for same-page query-param changes like tabs and filters. The host renders this as a LiveView patch, so the URL and page content update without a full browser reload.

<Link
  href={Routes.page(ctx, "work-weeks", { query: { status: "closed" } })}
  mode="patch"
  variant="button"
>
  Closed
</Link>

Use mode="navigate" for same-origin page-to-page navigation when you want LiveView navigation semantics. LiveView modes require a same-origin href; external links must use the default "link" mode.

Forms

<Form>

Wraps form fields and binds them to a UI action handler.

PropTypeNotes
actionstringRequired. The action id to invoke on submit.
cancelActionstringOptional cancel-action id.
cancelHrefstringOptional cancel link.
autoSubmit"input" | "change"Optional. Invoke the form action automatically when a field emits that event.
debounceMsnumberOptional debounce for autosubmit. Defaults to 500 for "input" and 0 for "change".
childrennodeField rows + submit button.
<Form action="save-vendor" cancelHref="/vendors">
  <Field name="name" label="Vendor name" required>
    <TextInput name="name" />
  </Field>
  <SubmitButton>Create</SubmitButton>
</Form>

Form fields are serialized into ctx.values. Button-specific params are not merged into ctx.values; they are available at ctx.action.params, which keeps route params, clicked-button params, and editable form values from colliding.

Use autoSubmit="change" for inline edit rows where a visible Save button would be noise. The action can return refresh() to re-render the current page without navigating.

<Form action="update-time-entry" autoSubmit="change">
  <HiddenInput name="entryId" value={entry.id} />
  <Field name="hours" label="Hours">
    <NumberInput defaultValue={entry.hours} min={0} step={0.25} />
  </Field>
</Form>

<Field>

Wraps a single input with a label, help text, and validation rules.

PropTypeNotes
namestringRequired. Form field name (key in ctx.values).
labelstringRequired.
helpTextstringSubtitle under the field.
requiredbooleanValidate non-empty before invoking the action.
minLengthnumberString length validation.
maxLengthnumberString length validation.
patternstringRegex string the value must match.
span1 - 12Optional grid column span on medium screens and up.
labelHiddenbooleanVisually hide the label while keeping it available to assistive tech.
childrennodeThe input — HiddenInput, NumberInput, DateInput, DateTimeInput, TextInput, Textarea, Select, Checkbox, or Toggle.
<Field name="email" label="Email" required pattern="^[^@]+@[^@]+$" helpText="Used for invoices.">
  <TextInput name="email" type="email" />
</Field>

<HiddenInput>

Hidden form value. Use this for form-scoped IDs that should submit without rendering an editable field.

PropTypeNotes
namestringRequired. Key in ctx.values.
valuestring | number | booleanRequired.
<Form action="save-hours">
  <HiddenInput name="entryId" value={entry.id} />
  <SubmitButton>Save</SubmitButton>
</Form>

<NumberInput>

Numeric input with browser number affordances.

PropTypeDefault
defaultValuestring | number | boolean
minnumber
maxnumber
stepnumber
readOnlybooleanfalse
disabledbooleanfalse
<NumberInput defaultValue={1.25} min={0} step={0.25} />

<DateInput>

Date input that submits ISO YYYY-MM-DD strings.

PropTypeDefault
defaultValuestring
minstring
maxstring
readOnlybooleanfalse
disabledbooleanfalse
<DateInput defaultValue={weekStart} min={weekStart} max={weekEnd} />

<DateTimeInput>

Datetime input that submits browser datetime-local strings.

PropTypeDefault
defaultValuestring
minstring
maxstring
readOnlybooleanfalse
disabledbooleanfalse
<DateTimeInput defaultValue="2026-05-11T09:00" />

<TextInput>

Single-line text input.

PropTypeDefault
defaultValuestring | number | boolean
placeholderstring
type"text" | "number" | "email" | "url" | "password""text"
readOnlybooleanfalse
disabledbooleanfalse
<TextInput name="taxId" placeholder="GB123456789" />

<Textarea>

Multi-line text input.

PropTypeDefault
defaultValuestring
placeholderstring
rowsnumberplatform default
readOnlybooleanfalse
disabledbooleanfalse
<Textarea name="notes" rows={4} placeholder="Internal notes…" />

<Select>

Dropdown.

PropTypeNotes
defaultValuestring
optionsSelectOption[]Required. { value, label }.
disabledbooleanfalse
<Select
  defaultValue="USD"
  options={[
    { value: "USD", label: "US Dollar" },
    { value: "EUR", label: "Euro" },
    { value: "GBP", label: "British Pound" },
  ]}
/>

<Checkbox>

Boolean checkbox. Submitted as "true" / "false" in ctx.values.

PropTypeDefault
checkedbooleanfalse
disabledbooleanfalse
<Field name="active" label="Active">
  <Checkbox checked={existing.active} />
</Field>

<Toggle>

Switch-style boolean. Same submit shape as <Checkbox>.

PropTypeDefault
checkedbooleanfalse
disabledbooleanfalse
<Toggle checked={settings.notifications} />

Action result helpers

Imported from the same module, used inside action handlers (not render scripts):

import { ok, message, redirect, refresh, validationError } from "@backfill-io/sdk/ui";
HelperReturns
ok(message?, opts?)Action succeeded. Optional toast.
message(text, level?)Toast at level "info" / "success" / "warning" / "error".
redirect(href, message?, level?)Navigate to href with optional toast.
refresh(message?, level?)Re-render the current extension page with optional toast.
validationError(error, fieldErrors[])Per-field validation result. Each entry: { field, message }.
import { Vendor } from "@backfill-io/sdk";
import { redirect, validationError } from "@backfill-io/sdk/ui";

export default async function saveVendor(ctx) {
  if (!ctx.values.name) {
    return validationError("Please fix the errors", [
      { field: "name", message: "Required" },
    ]);
  }
  Vendor.create({ name: ctx.values.name });
  return redirect("/vendors", "Vendor created", "success");
}

Shared types

type ScalarValue = string | number | boolean;

type TableColumn = {
  key: string;
  label: string;
  align?: "start" | "center" | "end";
  cell?:
    | { kind: "badge"; tone?: "info" | "success" | "warning" | "error" | "neutral" }
    | { kind: "link"; hrefKey: string; labelKey?: string }
    | { kind: "action"; action: string; label?: string; labelKey?: string; params?: Record<string, ScalarValue>; paramsKey?: string }
    | { kind: "money"; currency?: string; currencyKey?: string }
    | { kind: "date" };
};

type SelectOption = { value: string; label: string };