ComponentsPanel
Fieldset
A bordered box with a legend on its edge that groups related content, and can fold it away.
Import
import { Fieldset, FieldsetLegend, FieldsetContent } from "@booleanpress/ui/fieldset"Usage
A fieldset is a native <fieldset> whose <legend> sits on the box's top edge. Inside a form, it groups related fields under one name, which screen readers announce on entering the group. For a group of fields without a box, use FieldSet from field.
import { Fieldset, FieldsetContent, FieldsetLegend } from "@booleanpress/ui/fieldset"
export function Sender() {
return (
<Fieldset toggleable>
<FieldsetLegend>Sender</FieldsetLegend>
<FieldsetContent>From Acme Support, support@example.com.</FieldsetContent>
</Fieldset>
)
}With toggleable, the legend becomes a button that shows and hides the content, with a minus while open and a plus while closed; indicator replaces them. It is uncontrolled with defaultOpen (true by default), or controlled with open and onOpenChange. A fieldset takes the native attributes: disabled disables every control inside it except those in the legend, so the toggle keeps working, and name and form work as on any fieldset. Folded content stays in the page, hidden, so its fields keep what was typed and are still submitted with the form. The content's height animates from Radix's measured --radix-collapsible-content-height.
Examples
Basic
A legend on the edge of a box holding an invoice's lines.
import { Fieldset, FieldsetContent, FieldsetLegend } from "@booleanpress/ui/fieldset"
import { Separator } from "@booleanpress/ui/separator"
export default function FieldsetBasic() {
return (
<Fieldset className="w-full max-w-xs">
<FieldsetLegend>Invoice #1024</FieldsetLegend>
<FieldsetContent>
<div className="flex flex-col p-2">
<div className="flex justify-between">
<span>Growth plan</span>
<span className="text-muted-foreground">$29.00</span>
</div>
<div className="mt-3 flex justify-between">
<span>Dedicated IP</span>
<span className="text-muted-foreground">$5.99</span>
</div>
<Separator className="my-3.5" />
<div className="flex justify-between font-medium">
<span>Total</span>
<span>$34.99</span>
</div>
</div>
</FieldsetContent>
</Fieldset>
)
}Toggleable
toggleable makes the legend a button: the minus folds the content away, the plus brings it back.
import { Fieldset, FieldsetContent, FieldsetLegend } from "@booleanpress/ui/fieldset"
import { Separator } from "@booleanpress/ui/separator"
export default function FieldsetToggleable() {
return (
<Fieldset toggleable className="w-full max-w-xs">
<FieldsetLegend>Invoice #1024</FieldsetLegend>
<FieldsetContent>
<div className="flex flex-col p-2">
<div className="flex justify-between">
<span>Growth plan</span>
<span className="text-muted-foreground">$29.00</span>
</div>
<div className="mt-3 flex justify-between">
<span>Dedicated IP</span>
<span className="text-muted-foreground">$5.99</span>
</div>
<Separator className="my-3.5" />
<div className="flex justify-between font-medium">
<span>Total</span>
<span>$34.99</span>
</div>
</div>
</FieldsetContent>
</Fieldset>
)
}Controlled
open and onOpenChange keep the state outside, so other buttons can open and close it.
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import { Fieldset, FieldsetContent, FieldsetLegend } from "@booleanpress/ui/fieldset"
export default function FieldsetControlled() {
const [open, setOpen] = useState(true)
return (
<div className="flex w-full max-w-xs flex-col gap-4">
<div className="flex justify-center gap-2">
<Button onClick={() => setOpen(true)}>Open</Button>
<Button variant="secondary" onClick={() => setOpen(false)}>
Close
</Button>
</div>
<Fieldset toggleable open={open} onOpenChange={setOpen}>
<FieldsetLegend>Bounce handling</FieldsetLegend>
<FieldsetContent>
<p className="p-2">
Hard bounces add the address to the suppression list at once. Soft bounces are retried for 24 hours before the
address is suppressed.
</p>
</FieldsetContent>
</Fieldset>
</div>
)
}Custom indicator
indicator replaces the minus and plus with a chevron that turns.
import { ChevronDownIcon } from "lucide-react"
import { Fieldset, FieldsetContent, FieldsetLegend } from "@booleanpress/ui/fieldset"
// The legend's button is the Tailwind group `fieldset-trigger`: the chevron turns when the content opens.
const indicator = (
<ChevronDownIcon className="size-3.5 transition-transform duration-(--bui-duration-control) group-data-[state=open]/fieldset-trigger:rotate-180" />
)
export default function FieldsetCustomIndicator() {
return (
<Fieldset toggleable className="w-full max-w-xs">
<FieldsetLegend indicator={indicator}>Tracking</FieldsetLegend>
<FieldsetContent>
<p className="p-2">
Open and click tracking add a pixel and rewrite links. Both are off for transactional emails such as password
resets and receipts.
</p>
</FieldsetContent>
</Fieldset>
)
}With a form inside
Fields grouped under one legend, which screen readers read as the group's name.
import { Field, FieldGroup, FieldLabel } from "@booleanpress/ui/field"
import { Fieldset, FieldsetContent, FieldsetLegend } from "@booleanpress/ui/fieldset"
import { Input } from "@booleanpress/ui/input"
import { Switch } from "@booleanpress/ui/switch"
export default function FieldsetWithForm() {
return (
<Fieldset toggleable className="w-full max-w-sm">
<FieldsetLegend>Sender</FieldsetLegend>
<FieldsetContent>
<FieldGroup className="gap-4 pt-1">
<Field>
<FieldLabel htmlFor="sender-name">From name</FieldLabel>
<Input id="sender-name" defaultValue="Acme Support" />
</Field>
<Field>
<FieldLabel htmlFor="sender-email">From email</FieldLabel>
<Input id="sender-email" type="email" defaultValue="support@example.com" />
</Field>
<Field orientation="horizontal">
<Switch id="sender-force" defaultChecked />
<FieldLabel htmlFor="sender-force">Use this sender for every email</FieldLabel>
</Field>
</FieldGroup>
</FieldsetContent>
</Fieldset>
)
}Disabled
The native disabled attribute greys every field inside the fieldset and takes them out of the tab order.
import { Field, FieldGroup, FieldLabel } from "@booleanpress/ui/field"
import { Fieldset, FieldsetContent, FieldsetLegend } from "@booleanpress/ui/fieldset"
import { Input } from "@booleanpress/ui/input"
import { Switch } from "@booleanpress/ui/switch"
export default function FieldsetDisabled() {
return (
<Fieldset disabled className="w-full max-w-sm">
<FieldsetLegend>Sender</FieldsetLegend>
<FieldsetContent>
<FieldGroup className="gap-4 pt-1">
<Field>
<FieldLabel htmlFor="sender-locked-name">From name</FieldLabel>
<Input id="sender-locked-name" defaultValue="Acme Support" />
</Field>
<Field>
<FieldLabel htmlFor="sender-locked-email">From email</FieldLabel>
<Input id="sender-locked-email" type="email" defaultValue="support@example.com" />
</Field>
<Field orientation="horizontal">
<Switch id="sender-locked-force" defaultChecked />
<FieldLabel htmlFor="sender-locked-force">Use this sender for every email</FieldLabel>
</Field>
</FieldGroup>
</FieldsetContent>
</Fieldset>
)
}Accessibility
- Semantics
- A native
fieldset(rolegroup) named by itslegend. When toggleable, the legend holds abuttonwitharia-expandedandaria-controlspointing at the content; folded, the content ishidden: out of the focus order and the accessibility tree, but still in the page. - Labels
- The legend's text names the group, and, when toggleable, the button. Keep it short: screen readers repeat it on entering the group.
- Focus
- The legend's button is a tab stop before the content's controls. Focus is a 1px
--ringoutline 2px outside the legend. - Known limits
- Folded fields are still submitted with the form, and still checked by its validation: when the browser finds one invalid on submit, its section opens so the browser can move to the field and show its message. Validation of your own (
noValidateforms) should open the section itself, throughopen. - The indicator is decoration (
aria-hidden); the state is inaria-expanded.
- Folded fields are still submitted with the form, and still checked by its validation: when the browser finds one invalid on submit, its section opens so the browser can move to the field and show its message. Validation of your own (
Keyboard
| Key | Behaviour |
|---|---|
| EnterorSpace | On a toggleable legend: shows or hides the content. |
API
Fieldset
Renders a fieldset and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | true | Whether the content shows at the start, when the fieldset controls itself. true by default. |
onOpenChange | ((open: boolean) => void) | Called with the new state when the content is shown or hidden. | |
open | boolean | Whether the content shows, when you control it. Pair it with onOpenChange. Needs toggleable. | |
toggleable | boolean | false | Turns the legend into a button that shows and hides the content. false by default. |
FieldsetLegend
Renders a legend and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
indicator | ReactNode | Replaces the minus (open) and plus (closed) of a toggleable legend. The button is the Tailwind group
fieldset-trigger, so an icon can follow the state with group-data-[state=open]/fieldset-trigger:. |
FieldsetContent
Renders Radix Collapsible.Content and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
forceMount | true | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="fieldset" (Fieldset), data-slot="fieldset-legend" (FieldsetLegend), data-slot="fieldset-content" (FieldsetContent).
Theming
The box is --card with a 1px --border edge, a 6px radius and 16px padding; the legend is 14px semibold on --card. A toggleable legend takes --accent under the pointer, its icon --muted-foreground (--secondary-foreground under the pointer); focus is --ring.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent | background |
--accent-foreground | text |
--card | background |
--card-foreground | text |
--muted-foreground | text |
--ring | outline |
--secondary-foreground | text |