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>Examples
Default
A profile form with a header, a two-up name row, an email field, and a footer.
Variants
Four visual treatments — ghost (structure only), outline, filled, and elevated.
Sizes
Five sizes from xs to xl scale the title, padding, gaps, and footer spacing together.
Sections
Group related fields with FormSection, FormRow, and FormDivider inside a single form.
Submit error
FormSubmitError shows a tinted, bordered banner with role="alert" for top-level errors.
Loading
FormSkeleton reserves the form's chrome, header, fields, and footer while data loads.
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.
Flex
A low-level flexbox layout primitive that exposes the full flex API — four directions, three wrap modes, independent row and column gap, align-items, justify-content, align-content, inline mode, polymorphic render, and LTR/RTL. Ships with FlexItem for per-child grow, shrink, basis, order, and alignSelf control.
Grid
A CSS-grid layout primitive with static and responsive column counts, gap presets, flow control, and per-cell placement via GridItem. Polymorphic render, RTL, and no hard-coded tokens.