DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

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.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import {
  FieldsetRoot,
  FieldsetHeader,
  FieldsetLegend,
  FieldsetDescription,
  FieldsetBody,
  FieldsetHelperText,
} from "@tessinaui/ui";
<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 (M3/Carbon swap) — 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 start column beside the content from md up — the classic settings-page shape — and stacks on phones.

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 GOV.UK question 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.

Skeleton

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

When to use

Reach for Fieldset when a group of related controls shares one label, description, or validation message — notification preferences, permission lists, consent categories, plan selection. For a single labeled input, use Field. A grid of checkboxes under column headers is a Table job. A group that folds away composes Collapsible; a saved-form workflow around many fieldsets belongs to Form.

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
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 from md up; stacks on phones
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
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

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 (and rounded for Switch) 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.

Phone rhythm. Below md, 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 at md, and any other child (a Field, a nested Fieldset, raw markup) keeps it at every width, 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 (USWDS documents the fieldset gap on VO-iOS 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 (M3/Carbon), 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); Bootstrap documents the same 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.
  • AT verified: VoiceOver macOS. 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.
  • Atlassian's 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: Carbon's FormGroup and Atlaskit's Fieldset are invisible a11y wrappers; this component is also the settings-card unit.
FieldFile Upload

On this page

PlaygroundInstallationUsageExamplesDefaultVariantsSizesIntentsStatesValidationHeader actionsDivided rowsAnnotated layoutNested groupsFooter actionsCollapsible groupConsent categoriesQuestion pageCompositionSkeletonWhen to useAPI ReferenceFieldsetRoot propsFieldsetLegend propsFieldsetHeader propsFieldsetBody propsFieldsetDescription · FieldsetHelperText · FieldsetErrorTextFieldsetActions propsFieldsetSkeleton propsAccessibilityAgent notesMigrationNotes