Form
A complete form layout — header, body, sections, rows, footer, submit error banner — built on Base UI's Form primitive with four variants, five sizes, intent accents, validation modes, and full RTL support.
Playground
Installation
pnpm add @tessinaui/uiUsage
import {
FormRoot,
FormHeader,
FormTitle,
FormDescription,
FormBody,
FormSection,
FormRow,
FormDivider,
FormFooter,
FormSubmitError,
FormHelperText,
} from "@tessinaui/ui";<FormRoot
variant="outline"
onFormSubmit={(values) => save(values)}
validationMode="onBlur"
>
<FormHeader>
<FormTitle>Edit profile</FormTitle>
<FormDescription>Update your public information.</FormDescription>
</FormHeader>
<FormBody>
<FormRow>
<Field name="firstName" label="First name" required />
<Field name="lastName" label="Last name" required />
</FormRow>
<Field name="email" label="Email" type="email" required />
<Switch label="Email me product updates" defaultChecked />
</FormBody>
<FormFooter>
<Button variant="ghost">Cancel</Button>
<Button type="submit">Save changes</Button>
</FormFooter>
</FormRoot>Showcase
When to use
Form is the top-level container for any form-shaped interaction in your app — sign-in, sign-up, profile editing, settings panes, multi-section onboarding. It bundles:
- Visual chrome (variants, padding, intent border accent)
- Layout primitives (
Header,Body,Section,Row,Footer) - Submission state (
isSubmittingdisables everything and setsaria-busy) - Server-side error reporting (
errorsmap keyed by fieldname, plus aSubmitErrorbanner for top-level errors)
For a single grouped set of controls without form-level submission, use Fieldset. For an individual labeled input, use Field.
API Reference
FormRoot props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Scales title, padding, gap, footer spacing |
variant | "ghost" | "outline" | "filled" | "elevated" | "ghost" | Visual treatment — ghost is just structure, the others wrap everything in a card |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Card corner radius (only applies to non-ghost variants) |
intent | "none" | "primary" | "error" | "warning" | "success" | "info" | "none" | Drives border accent and helper text colour |
dir | "ltr" | "rtl" | "ltr" | Text direction |
layout | "vertical" | "horizontal" | "vertical" | Default Field label layout (Field components read this from context) |
validationMode | "onSubmit" | "onBlur" | "onChange" | "onSubmit" | When fields validate. Field.Root's own validationMode always wins |
errors | Record<string, string | string[]> | — | Server-side errors keyed by field name. Connect from your form action handler |
onFormSubmit | (values, eventDetails) => void | Promise<void> | — | Submit handler with parsed form values. Calls preventDefault() on the native submit event automatically |
actionsRef | RefObject<{ validate(name?) }> | — | Imperative validation — actionsRef.current.validate() runs all, validate('email') runs one |
isSubmitting | boolean | false | Disables the form and sets aria-busy="true" |
disabled | boolean | false | Disables every form control inside (cascades natively via an internal <fieldset>) |
FormHeader / Title / Description
FormHeader is a flex column wrapper. FormTitle renders an <h2> by default (configurable via level). FormDescription renders a muted <p> below the title.
FormBody props
A flex column with size-aware gap between children. Drop Field, Switch, Fieldset, FormSection, FormRow, etc. inside.
FormSection props
| Prop | Type | Description |
|---|---|---|
title | ReactNode | Optional section title (renders as <h3>) |
description | ReactNode | Optional muted helper text below the title |
A semantic <section> block grouping related fields under their own title and description. Use it for thematic grouping inside a form (e.g. "Profile" / "Notifications" / "Permissions") when you don't want full Fieldset chrome.
FormRow props
| Prop | Type | Default | Description |
|---|---|---|---|
stackOnMobile | boolean | true | Stacks children vertically below the sm breakpoint, then switches to a row at sm+. Set false for always-horizontal layouts |
Lays children out side-by-side with flex: 1 so each takes equal width.
FormFooter props
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "between" | "end" | Horizontal alignment of footer children |
Adds size-aware top spacing and gap. Drop Buttons, Links, or any node here.
FormSubmitError props
| Prop | Type | Default | Description |
|---|---|---|---|
intent | FormIntent | "error" | Colour palette and default icon |
title | ReactNode | — | Bold title above the body text |
hideIcon | boolean | false | Render without the leading icon |
A bordered, tinted banner for top-level form errors (e.g. "Couldn't sign in"). Sets role="alert" for screen readers.
FormHelperText props
| Prop | Type | Default | Description |
|---|---|---|---|
intent | FormIntent | inherited | Override the helper text colour for this instance only |
leadingIcon | ReactNode | — | Icon rendered before the text |
A small subtle line below the form (terms, fine print, "all changes save automatically", etc).
FormDivider
A 1px horizontal separator with size-aware vertical spacing — use between major sections inside a long form.
Composition tips
FormRowis for putting two short fields side-by-side (e.g. first/last name). For full vertical stacks, just useFormBodydirectly.FormSectionandFieldsetoverlap —FormSectionis a lighter weight semantic group (no<fieldset>element, no disabled cascade),Fieldsetis the real form-grouping primitive with nativedisabledpropagation.- The submit handler receives parsed form values via Base UI's typed callback. To handle async server validation, return a Promise and surface results through
errors. - Keep cognitive load down (NN/g): chunk fields into
FormSections, defer optional fields behind aCollapsible, and put primary action on the right (LTR) or left (RTL).
Notes
- Built on
@base-ui/react/form. Errors flow through Base UI'sFormContextto descendantField.Roots — setnameon each Field that should map to a server error key. - The
disabledprop wraps children in an internal<fieldset disabled>so every native form control inside gets the disabled state for free. - For multi-step forms, render multiple
FormRoots and switch between them with state — each step gets its ownonFormSubmit.