Tessina UI
Tessina UI
GitHubIntroduction
InstallationUsageTheming
Components
ButtonIconButtonLinkSpinnerBadgeAvatarStatusChipShortcutSkeletonSurfaceProgressMeterRating
LabelFieldFieldsetCheckboxRadioSwitchSliderSelectComboboxSearchTextareaNumberFieldDate PickerTime PickerOTP InputFile UploadCalendar
AlertBannerToastTooltip
TabsAccordionCollapsibleBreadcrumbPaginationStepperSegmentedControlButtonGroupToggleButtonToggleGroupToolbarNavigation MenuMenubarBottom NavSidebar
Split ButtonFABDropdown MenuContextMenuCommandPopoverHoverCard
ContainerStackFlexGridAspectRatioSpacerCardCarouselDividerScroll Area
Table
ChatBubblePromptInputCodeBlock
ModalAlertDialogDrawerAction SheetTop Header MobileTop Header DesktopEmptyStateForm
Contributing
ComponentsSurfaces

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/ui

Usage

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 (isSubmitting disables everything and sets aria-busy)
  • Server-side error reporting (errors map keyed by field name, plus a SubmitError banner 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

PropTypeDefaultDescription
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
errorsRecord<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
actionsRefRefObject<{ validate(name?) }>—Imperative validation — actionsRef.current.validate() runs all, validate('email') runs one
isSubmittingbooleanfalseDisables the form and sets aria-busy="true"
disabledbooleanfalseDisables 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

PropTypeDescription
titleReactNodeOptional section title (renders as <h3>)
descriptionReactNodeOptional 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

PropTypeDefaultDescription
stackOnMobilebooleantrueStacks 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

PropTypeDefaultDescription
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

PropTypeDefaultDescription
intentFormIntent"error"Colour palette and default icon
titleReactNode—Bold title above the body text
hideIconbooleanfalseRender 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

PropTypeDefaultDescription
intentFormIntentinheritedOverride the helper text colour for this instance only
leadingIconReactNode—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

  • FormRow is for putting two short fields side-by-side (e.g. first/last name). For full vertical stacks, just use FormBody directly.
  • FormSection and Fieldset overlap — FormSection is a lighter weight semantic group (no <fieldset> element, no disabled cascade), Fieldset is the real form-grouping primitive with native disabled propagation.
  • 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 a Collapsible, and put primary action on the right (LTR) or left (RTL).

Notes

  • Built on @base-ui/react/form. Errors flow through Base UI's FormContext to descendant Field.Roots — set name on each Field that should map to a server error key.
  • The disabled prop 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 own onFormSubmit.

EmptyState

Centered surface for "no data here" — empty lists, search results, tables, dashboards, inboxes, 404s, first-run / onboarding moments. Three variants, four sizes, six intents tinting the icon tile, primary + secondary actions, optional footer, LTR/RTL.

Contributing

Help improve Tessina UI

On this page

PlaygroundInstallationUsageShowcaseWhen to useAPI ReferenceFormRoot propsFormHeader / Title / DescriptionFormBody propsFormSection propsFormRow propsFormFooter propsFormSubmitError propsFormHelperText propsFormDividerComposition tipsNotes