UI componentsdocs/ui/components/forms.md

Forms

Structure a form: labels, help and errors, sections, footers and settings rows.

← Component reference

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

PropTypeRequiredDefaultDescription
addonsbooleanAttach controls edge-to-edge (Bulma has-addons).
childrencontentThe control(s), e.g. a TextInput.
errorstringError message; replaces help, marks the control invalid.
expandedbooleanGrow to fill the row.
groupedbooleanGroup controls with gap (Bulma is-grouped).
groupedMultilinebooleanWith grouped: wrap the controls onto several lines.
helpstringHint under the control.
horizontalbooleanHorizontal label + body (Bulma field is-horizontal).
labelcontentLabel text or content.
labelForstringId of the control the label names; also links help / error via aria-describedby.
narrowbooleanOnly 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>
    );
}
PropTypeRequiredDefaultDescription
childrencontentThe input and its icons.
expandedbooleanGrow to fill the row.
iconsLeftbooleanReserve space for a leading icon inside the input.
iconsRightbooleanReserve 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>
    );
}
PropTypeRequiredDefaultDescription
childrencontentHelp text.
tone"success" | "danger" | "muted""muted"Colour: muted hint, success or danger.

Label #

Form label.

<Label for="project-name">
    Project name
</Label>
PropTypeRequiredDefaultDescription
childrencontentLabel text.
forstringId 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

PropTypeRequiredDefaultDescription
childrencontentThe section's fields.
descriptioncontentText under the heading.
order2 | 3 | 4 | 5 | 63Heading level of the title, to fit the page outline. Default 3.
titlestringSection 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

PropTypeRequiredDefaultDescription
borderedbooleantrueVisually emphasize as a bordered panel. Default true.
childrencontentThe area's fields.
descriptioncontentText under the heading.
order2 | 3 | 4 | 5 | 64Heading level of the title, to fit the page outline. Default 4.
titlestringArea heading.

Fieldset #

Native fieldset with optional legend.

Shipping address
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

PropTypeRequiredDefaultDescription
childrencontentThe grouped fields.
disabledbooleanDisables every control inside.
legendcontentGroup caption (<legend>).

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

FormFooter #

Right-aligned form actions row.

Interact with the example: callbacks show up here.
<form
    onSubmit={(e: SubmitEvent) => {
        e.preventDefault();
        () => {}();
    }}
>
    <FormFooter>
        <Button variant="ghost" onClick={() => {}}>
            Cancel
        </Button>
        <Button type="submit">Save changes</Button>
    </FormFooter>
</form>
PropTypeRequiredDefaultDescription
childrencontentForm 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.

Default branch
Pushes to this branch deploy to production.
main
<SettingsRow
    label="Default branch"
    description="Pushes to this branch deploy to production."
    control={<Code>main</Code>}
/>

Slots: root control description label text

PropTypeRequiredDefaultDescription
labelcontentyesSetting name.
controlcontentThe control at the end of the row (switch, select, button, …).
descriptioncontentExplanation under the name.
descriptionIdstringId for the description, to point a control's aria-describedby at.
labelIdstringIds for the label / description, so the control can reference them.

ToggleRow #

SettingsRow with a Switch.

Preview deploys
Deploy every pull request to a unique URL.
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}
        />
    );
}
PropTypeRequiredDefaultDescription
checkedbooleanyesWhether the setting is on (controlled).
labelcontentyesSetting name (the switch's accessible name).
onChange(checked: boolean) => voidyesCalled with the new state.
descriptioncontentExplanation under the name (the switch's accessible description).
disabledbooleanDisables the switch.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

DangerZone #

Destructive-settings section.

Delete project

This permanently removes all deploys and domains.

Interact with the example: callbacks show up here.
<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

PropTypeRequiredDefaultDescription
childrencontentThe destructive control, e.g. a danger Button.
descriptioncontentWhat the destructive action does.
titlestring"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

PropTypeRequiredDefaultDescription
backLabelcontent"Back"Back button text.
canBackbooleanEnables the Back button.
canNextbooleanEnables the Next button.
nextLabelcontent"Continue"Next button text (e.g. "Finish" on the last step).
onBack() => voidCalled when Back is pressed.
onNext() => voidCalled when Next is pressed.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.