Forms
Structure a form: labels, help and errors, sections, footers and settings rows.
Form field #
Label, control, help and error text, linked for assistive tech.
FormField #
Labelled form control wrapper: label, control, help or error text, with aria-describedby / aria-invalid wired to the control.
Lowercase letters, numbers and dashes.
function Example() {
const project = signal("marketing-site");
const invalid = () => !/^[a-z0-9-]+$/.test(project());
return (
<FormField
label="Project name"
labelFor="field-project"
help="Lowercase letters, numbers and dashes."
error={invalid() ? "Use only lowercase letters, numbers and dashes." : undefined}
>
<TextInput
id="field-project"
invalid={invalid()}
value={project()}
onInput={(e) => project.set((e.target as HTMLInputElement).value)}
/>
</FormField>
);
}Slots: root body help inner label
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
addons | boolean | Attach controls edge-to-edge (Bulma has-addons). | ||
children | content | The control(s), e.g. a TextInput. | ||
error | string | Error message; replaces help, marks the control invalid. | ||
expanded | boolean | Grow to fill the row. | ||
grouped | boolean | Group controls with gap (Bulma is-grouped). | ||
groupedMultiline | boolean | With grouped: wrap the controls onto several lines. | ||
help | string | Hint under the control. | ||
horizontal | boolean | Horizontal label + body (Bulma field is-horizontal). | ||
label | content | Label text or content. | ||
labelFor | string | Id of the control the label names; also links help / error via aria-describedby. | ||
narrow | boolean | Only as wide as its content. |
Parts
Control #
Single control wrapper (Bulma control).
function Example() {
const email = signal("");
return (
<FormField label="Email" labelFor="control-email">
<Control expanded>
<TextInput
id="control-email"
type="email"
placeholder="[email protected]"
value={email()}
onInput={(e) => email.set((e.target as HTMLInputElement).value)}
/>
</Control>
</FormField>
);
}| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | content | The input and its icons. | ||
expanded | boolean | Grow to fill the row. | ||
iconsLeft | boolean | Reserve space for a leading icon inside the input. | ||
iconsRight | boolean | Reserve space for a trailing icon inside the input. |
Help #
Field help / validation text (Bulma help).
This username is available.
function Example() {
const user = signal("ada");
const taken = () => ["admin", "root"].includes(user());
return (
<FormField label="Username" labelFor="help-user">
<TextInput
id="help-user"
value={user()}
onInput={(e) => user.set((e.target as HTMLInputElement).value)}
/>
<Help tone={taken() ? "danger" : "success"}>
{taken() ? "That username is taken." : "This username is available."}
</Help>
</FormField>
);
}| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | content | Help text. | ||
tone | "success" | "danger" | "muted" | "muted" | Colour: muted hint, success or danger. |
Label #
Form label.
<Label for="project-name">
Project name
</Label>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | content | Label text. | ||
for | string | Id of the control this label names. |
Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.
Form sections #
Group related fields.
FormSection #
Titled form region (grouping of related fields).
Profile
Shown on your public page.
function Example() {
const name = signal("Ada Lovelace");
return (
<FormSection title="Profile" description="Shown on your public page.">
<FormField label="Display name" labelFor="section-name">
<TextInput
id="section-name"
value={name()}
onInput={(e) => name.set((e.target as HTMLInputElement).value)}
/>
</FormField>
</FormSection>
);
}Slots: root body description header title
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | content | The section's fields. | ||
description | content | Text under the heading. | ||
order | 2 | 3 | 4 | 5 | 6 | 3 | Heading level of the title, to fit the page outline. Default 3. | |
title | string | Section heading. |
FormArea #
Grouped form area / panel (related fields in a boxed region).
Notifications
Choose what we email you about.
function Example() {
const failed = signal(true);
const weekly = signal(false);
return (
<FormArea
order={3}
title="Notifications"
description="Choose what we email you about."
bordered
>
<Checkbox
label="Failed deploys"
checked={failed()}
onChange={(e) => failed.set((e.target as HTMLInputElement).checked)}
/>
<Checkbox
label="Weekly summary"
checked={weekly()}
onChange={(e) => weekly.set((e.target as HTMLInputElement).checked)}
/>
</FormArea>
);
}Slots: root body description header title
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
bordered | boolean | true | Visually emphasize as a bordered panel. Default true. | |
children | content | The area's fields. | ||
description | content | Text under the heading. | ||
order | 2 | 3 | 4 | 5 | 6 | 4 | Heading level of the title, to fit the page outline. Default 4. | |
title | string | Area heading. |
Fieldset #
Native fieldset with optional legend.
function Example() {
const street = signal("");
const city = signal("");
return (
<Fieldset legend="Shipping address">
<FormField label="Street" labelFor="fs-street">
<TextInput
id="fs-street"
value={street()}
onInput={(e) => street.set((e.target as HTMLInputElement).value)}
/>
</FormField>
<FormField label="City" labelFor="fs-city">
<TextInput
id="fs-city"
value={city()}
onInput={(e) => city.set((e.target as HTMLInputElement).value)}
/>
</FormField>
</Fieldset>
);
}Slots: root legend
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | content | The grouped fields. | ||
disabled | boolean | Disables every control inside. | ||
legend | content | Group caption (<legend>). |
Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.
FormFooter #
Right-aligned form actions row.
<form
onSubmit={(e: SubmitEvent) => {
e.preventDefault();
() => {}();
}}
>
<FormFooter>
<Button variant="ghost" onClick={() => {}}>
Cancel
</Button>
<Button type="submit">Save changes</Button>
</FormFooter>
</form>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | content | Form buttons, aligned to the end. |
Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.
Settings rows #
Label + control rows for settings pages.
SettingsRow #
Label + description + control row.
main<SettingsRow
label="Default branch"
description="Pushes to this branch deploy to production."
control={<Code>main</Code>}
/>Slots: root control description label text
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | content | yes | Setting name. | |
control | content | The control at the end of the row (switch, select, button, …). | ||
description | content | Explanation under the name. | ||
descriptionId | string | Id for the description, to point a control's aria-describedby at. | ||
labelId | string | Ids for the label / description, so the control can reference them. |
ToggleRow #
SettingsRow with a Switch.
function Example() {
const previews = signal(true);
return (
<ToggleRow
label="Preview deploys"
description="Deploy every pull request to a unique URL."
checked={previews()}
onChange={previews.set}
/>
);
}| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
checked | boolean | yes | Whether the setting is on (controlled). | |
label | content | yes | Setting name (the switch's accessible name). | |
onChange | (checked: boolean) => void | yes | Called with the new state. | |
description | content | Explanation under the name (the switch's accessible description). | ||
disabled | boolean | Disables the switch. |
Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.
DangerZone #
Destructive-settings section.
This permanently removes all deploys and domains.
<DangerZone
title="Delete project"
description="This permanently removes all deploys and domains."
>
<Button variant="danger" onClick={() => {}}>
Delete marketing-site
</Button>
</DangerZone>Slots: root body description header title
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | content | The destructive control, e.g. a danger Button. | ||
description | content | What the destructive action does. | ||
title | string | "Danger zone" | Section heading. |
Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.
WizardNav #
Back / Continue footer for multi-step flows.
Step 1 of 4: Account
function Example() {
const steps = ["Account", "Team", "Billing", "Done"];
const step = signal(0);
return (
<Stack gap="0.75rem">
<Text>
Step {step() + 1} of {steps.length}: <strong>{steps[step()]}</strong>
</Text>
<WizardNav
canBack={step() > 0}
canNext={step() < steps.length - 1}
nextLabel={step() === steps.length - 2 ? "Finish" : "Continue"}
onBack={() => step.set(step() - 1)}
onNext={() => step.set(step() + 1)}
/>
</Stack>
);
}Slots: root back next
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
backLabel | content | "Back" | Back button text. | |
canBack | boolean | Enables the Back button. | ||
canNext | boolean | Enables the Next button. | ||
nextLabel | content | "Continue" | Next button text (e.g. "Finish" on the last step). | |
onBack | () => void | Called when Back is pressed. | ||
onNext | () => void | Called when Next is pressed. |
Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.