DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
FieldFieldsetFormFile UploadColor PickerMediaTrimmer
AI agentsContributing
  1. Components
  2. Fieldset

Fieldset

Groups related form controls under a shared legend, description, helper and error text — a native fieldset with real disabled cascade, aria-describedby wiring, card variants, and annotated settings layout.

$ pnpm add @tessinaui/ui

When to use

  • Related controls that share one label. Notification preferences, permission lists, consent categories, plan selection: a legend names the group once, so each control can keep a short label.
  • A group-level description, helper or error. errorMessage joins the group's aria-describedby and replaces the helper in place, so one message covers the whole set.
  • Disabling a set together. disabled renders the native attribute and cascades onto the known control children.
  • A settings card. The outline, filled and elevated variants make the group a panel; layout="annotated" puts the header beside the rows on wide screens.
  • A page that asks one question. Put the heading inside the legend so the page heading and the group's name are the same node (Question page).
  • Sub-groups inside a group. Nested fieldsets take the compact variant="label" legend.

When not to use

  • For a single labelled input. Use Field; the label already names it.
  • To draw a box. If you only want a border or background around unrelated content, use a Card or a Surface. A fieldset is a grouping with meaning, not decoration.
  • For a grid of checkboxes under column headers. That is a Table job.
  • Around a CheckboxGroup or RadioGroup that already has its own label. They name themselves; wrap them only when the fieldset adds a shared description or error (Composition).
  • For a group that folds away. Compose Collapsible inside the body (Collapsible group).
  • For a saved-form workflow around many fieldsets. That belongs to Form.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import {
  FieldsetRoot,
  FieldsetHeader,
  FieldsetLegend,
  FieldsetDescription,
  FieldsetBody,
  FieldsetHelperText,
} from "@tessinaui/ui";
example.tsx
<FieldsetRoot variant="outline">
  <FieldsetHeader>
    <FieldsetLegend required>Notification preferences</FieldsetLegend>
    <FieldsetDescription>
      Choose how you want to be notified about updates.
    </FieldsetDescription>
  </FieldsetHeader>

  <FieldsetBody>
    <Switch label="Email" defaultChecked />
    <Switch label="SMS" />
    <Switch label="Mobile push" />
  </FieldsetBody>

  <FieldsetHelperText>
    You can change these later in account settings.
  </FieldsetHelperText>
</FieldsetRoot>

Examples

Default

A grouped set of switches under a shared legend and description.

Variants

Card treatments — outline, filled, and elevated (the default ghost is structure only). Space alone groups; reach for a card only when the page needs the stronger separation.

Sizes

Five sizes from xs to xl, scaling the legend, text, gap, and padding together — and cascading onto Switch, Checkbox and Radio children.

Intents

Intent drives the border accent (an inset ring on filled, so nothing shifts) and the default HelperText colour.

States

disabled renders the native <fieldset disabled> attribute and cascades onto known control children; errorMessage swaps the helper text for a politely-announced error.

Validation

Group-level errorMessage joins the fieldset's aria-describedby, implies the error intent, and replaces the helper in place — plus a visually hidden legend that still names the group.

Header actions

The Header trailing slot: a master switch arming the group, a reset action, or a count badge.

Divided rows

dividers on the Body swaps the open gap for rules between rows — the settings-list rhythm.

Annotated layout

layout="annotated" puts the header in a 15rem start column beside the content — the classic settings-page shape — once the fieldset itself is at least 36rem wide, and stacks when it is narrower. It follows its own width, not the screen's, so it also stacks in a narrow column on a wide screen.

Nested groups

Nested fieldsets carry compact variant="label" legends, so sub-groups read as one section.

Footer actions

FieldsetActions is the Save/Cancel row at the group's bottom.

Collapsible group

Compose the house Collapsible around the body — the legend stays visible, only the rows fold.

Consent categories

The cookie-preferences shape: a locked "always active" row (readOnly Switch with a status word), editable categories, footer actions.

Question page

The one-question-per-page shape: a heading inside the legend, so the page heading and the group's accessible name are the same node.

Composition

CheckboxGroup and RadioGroup compose naturally inside the body.

On color

A ghost fieldset paints no panel, so inside a coloured Surface its legend and description take the plate's inks and its controls inherit on-color. outline, filled and elevated paint a page panel and hand their content the default tone.

Skeleton

Silent by default; pass label for the one announced loading region.

RTL

dir="rtl" aligns the legend, description and helper text right and moves the header's trailing action to the left edge. Controls inside inherit the direction without a dir of their own. Unset, dir is not stamped and the fieldset follows the page.

API Reference

FieldsetRoot props

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Scales legend, description, helper text, gap, padding — and child controls via the cascade
variant"ghost" | "outline" | "filled" | "elevated""ghost"ghost is structure only; the others wrap everything in a card. filled is a card for a grouped page — put it on bg-muted in light and bg-background in dark
rounded"none" | "sm" | "md" | "lg" | "full""md"Card corner radius (non-ghost). full is the 24px panel radius, per the house §2 panel rule
intent"none" | "error" | "warning" | "success" | "info""none"Border accent (inset ring on filled) + default HelperText colour. "primary" still renders as a deprecated alias for one minor and warns in dev
layout"stacked" | "annotated""stacked"annotated puts the header beside the content once the fieldset is 36rem wide; stacks when narrower
orientation"vertical" | "horizontal""vertical"Default layout for FieldsetBody children
disabledbooleanfalseRenders the native <fieldset disabled> attribute and clones disabled onto known control children — see the cascade note below
invalidbooleanfalseError styling without any message text
errorMessageReactNode—Group error: swaps the helper slot, announces politely, joins aria-describedby, implies the error intent unless another non-none intent is set
dir"ltr" | "rtl"inheritedOnly set to force a direction; unset inherits from the document
stringsPartial<FieldsetStrings>English, Arabic under dir="rtl"Override the legend's markers: required (the screen-reader-only word) and optional ("(optional)")
renderBase UI render prop—Replaces the rendered element — the replacement then owns its own attributes, including native disabled

FieldsetLegend props

PropTypeDefaultDescription
variant"legend" | "label""legend"label is the compact field-label scale for nested sub-groups
requiredbooleanfalseDecorative * plus a screen-reader-only " required" in the group name
optionalbooleanfalseMuted "(optional)" joined into the group name. Ignored when required
visuallyHiddenbooleanfalseHides the legend visually; the group keeps its accessible name
leadingIconReactNode—Icon before the legend text
stringsPartial<FieldsetStrings>the FieldsetRoot'sOverride required and optional for this legend only

Renders Base UI's Fieldset.Legend — a <div> the fieldset points at via aria-labelledby. See Accessibility for what that means in practice.

FieldsetHeader props

PropTypeDefaultDescription
trailingReactNode—Master switch, action button, or count badge at the legend row's end

FieldsetBody props

PropTypeDefaultDescription
orientation"vertical" | "horizontal"inheritedOverride the Root's orientation for this body only
dividersbooleanfalseRules between rows instead of the open gap (vertical only). Use sparingly

The body also applies the cascade: size plus disabled are cloned onto direct children named Switch, Checkbox(Group/Card/CardGroup), Radio(Group/Card/CardGroup) — fragments are traversed, deeper wrappers are not. Explicit props on a child always win. The fieldset's rounded shapes its own card only: switches, checkboxes and radios keep their own shape at every value.

Touch rhythm. On a touch screen — a phone, or a coarse pointer at any width — a vertical body whose children are all single control rows (Switch, Checkbox, Radio) drops its gap and stamps data-rows: those rows already carry a 44px touch floor, so a gap on top double-counts it — a 24px switch in a 44px row plus a 12px gap reads as 56px of pitch, the sparse list every phone platform avoids. The gap returns with a mouse or trackpad from md up, where no floor applies, and any other child (a Field, a nested Fieldset, raw markup) keeps it everywhere, because those have no floor of their own. CheckboxGroup applies the same rule to its own rows.

FieldsetDescription · FieldsetHelperText · FieldsetErrorText

Description (muted, under the legend), HelperText (bottom meta row, intent-coloured, optional leadingIcon, hidden while the Root's errorMessage is present) and ErrorText (error-coloured row; rendered for you by errorMessage, composable when you own the layout). All three register their ids into the fieldset's aria-describedby.

FieldsetActions props

PropTypeDefaultDescription
align"start" | "center" | "end" | "between""end"Placement of the footer action row

FieldsetSkeleton props

variant / size / rounded / intent / orientation mirror the root; control ("field" \| "switch"), fields, showLegend / showDescription / showHelper shape the placeholder; label opts into the single polite announcement — silent without it.

Accessibility

  • The root is a real <fieldset> (role="group"), named by the legend via aria-labelledby. Base UI's legend is a <div>, not a native <legend>: desktop screen readers (VoiceOver macOS, NVDA, JAWS) resolve the name identically, but VoiceOver iOS and TalkBack are historically unreliable with group semantics either way (the fieldset gap on VO-iOS is documented even for native legends). Keep individual control labels self-explanatory — "Email" under a "Notifications" legend still reads as "Email" alone on the weakest AT.
  • Description, HelperText and ErrorText join the fieldset's aria-describedby; consumer-supplied aria-describedby is merged first, never clobbered.
  • errorMessage renders inside a pre-mounted aria-live="polite" region, so the insertion announces without stealing focus. The helper is swapped out, not stacked, so nothing shifts beside an open keyboard.
  • required on the legend adds a decorative star and the word "required" to the group name — role=group accepts no aria-required, and there is no single input to carry the native attribute.
  • Disabled coverage: disabled renders the native attribute (disabling native inputs, e.g. a Field's <input>) and clones disabled onto the known control children listed under Body. A custom component with a different displayName, or a control nested inside an extra wrapper <div>, receives neither — pass disabled yourself there. Anchor-styled buttons are never disabled by a fieldset (browser behaviour) — a well-documented trap.
  • A control in the header trailing slot that arms the group (a master switch) must not sit inside a disabled scope it controls — disable the rows, not the root (see the Header actions example).
  • Keyboard: the group itself takes no focus; Tab order is the natural order of the controls inside.
  • VoiceOver + Safari passed 2026-09-14: the first switch reads "Email, on, switch", then the group, "Notification preferences, Choose how you want to be notified about updates., group"; Space on the next switch reads "on, SMS, switch". NVDA and JAWS: untested (no Windows hardware in this loop).

Agent notes

Every part carries a stable data-slot (fieldset, fieldset-header, fieldset-legend, fieldset-description, fieldset-body, fieldset-helper, fieldset-error, fieldset-error-region, fieldset-actions, fieldset-header-trailing, fieldset-skeleton); the root also stamps data-invalid, data-disabled, data-layout, data-orientation, and the body data-rows when it holds only control rows. Target those, not class names. To read validation state: [data-slot="fieldset"][data-invalid], message text inside [data-slot="fieldset-error"].

Migration

BeforeAfter
intent="primary"Deprecated alias (renders + dev-warns for one minor) — use className for a brand accent
rounded="full" → 16pxNow the 24px panel radius (rounded-3xl), per the house §2 rule
dir forced "ltr"Unset inherits the document direction; set only to force one
Docs claimed native disabled cascadeIt now actually exists — native attribute + clone-cascade with the documented coverage above
Skeleton always announced "Loading"Silent by default — pass label to announce

Notes

  • Fieldset.Header is optional — a bare Fieldset.Legend directly in the root works.
  • The required-fields convention fits the Description: one line ("Required fields are marked with an asterisk *") instead of decorating every field.
  • The visually rich card variants are deliberate: a form-group wrapper is usually an invisible a11y wrapper; this component is also the settings-card unit.
FieldForm

On this page

When to useWhen not to useInstallationUsageExamplesDefaultVariantsSizesIntentsStatesValidationHeader actionsDivided rowsAnnotated layoutNested groupsFooter actionsCollapsible groupConsent categoriesQuestion pageCompositionOn colorSkeletonRTLAPI ReferenceFieldsetRoot propsFieldsetLegend propsFieldsetHeader propsFieldsetBody propsFieldsetDescription · FieldsetHelperText · FieldsetErrorTextFieldsetActions propsFieldsetSkeleton propsAccessibilityAgent notesMigrationNotes