Skip to content
ShineUI
Install

Fieldset

A legend on the border.

<sui-fieldset>PanelsStable

About

Bordered group of form fields with the legend riding the border line. Native fieldset and legend semantics, so a screen reader names every control inside with the legend. Reach for it to group related fields inside a longer form; collapsible folds the group and disabled turns off every control at once.

Usage

<sui-fieldset legend="Billing address"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="bill-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="bill-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="bill-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="bill-zip" label="ZIP" value="47150"></sui-input-text> </div> </sui-fieldset> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-fieldset"); el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiFieldset } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return ( <SuiFieldset legend="Billing address" onSuiClose={(e) => console.log(e.detail)}> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="bill-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="bill-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="bill-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="bill-zip" label="ZIP" value="47150"></sui-input-text> </div> </SuiFieldset> ); }
<script setup> import { SuiFieldset } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiFieldset legend="Billing address" @sui-close="onChange"><div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="bill-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="bill-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="bill-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="bill-zip" label="ZIP" value="47150"></sui-input-text> </div></SuiFieldset> </template>
import { Component } from "@angular/core"; import { SuiFieldset } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiFieldset], template: `<sui-fieldset legend="Billing address" [(collapsed)]="collapsed" (suiClose)="onChange($event)"><div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="bill-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="bill-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="bill-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="bill-zip" label="ZIP" value="47150"></sui-input-text> </div></sui-fieldset>`, }) export class Example { collapsed = false; onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

Native fieldset and legend semantics, so a screen reader names every control inside with the legend. The legend rides the border line.

<sui-fieldset legend="Billing address" style="max-width:460px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="bill-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="bill-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="bill-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="bill-zip" label="ZIP" value="47150"></sui-input-text> </div> </sui-fieldset>

Collapsible

collapsible puts a disclosure button in the legend, and the body unmounts while collapsed. Writing collapsed from script hides or shows the body and emits nothing.

<div style="display:flex;flex-direction:column;gap:14px;max-width:460px"> <div style="display:flex;gap:8px"> <sui-button id="fs-fold" variant="outline" size="sm" label="Collapse both"></sui-button> <sui-button id="fs-unfold" variant="outline" size="sm" label="Expand both"></sui-button> </div> <sui-fieldset class="fs-driven" collapsible legend="Billing address"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="open-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="open-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="open-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="open-zip" label="ZIP" value="47150"></sui-input-text> </div> </sui-fieldset> <sui-fieldset class="fs-driven" collapsible collapsed legend="Shipping address"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="ship-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="ship-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="ship-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="ship-zip" label="ZIP" value="47150"></sui-input-text> </div> </sui-fieldset> </div>
// setup(root): properties and events setup(root) { const groups = root.querySelectorAll('.fs-driven'); root.querySelector('#fs-fold').addEventListener('click', () => { groups.forEach((g) => { g.collapsed = true; }); }); root.querySelector('#fs-unfold').addEventListener('click', () => { groups.forEach((g) => { g.collapsed = false; }); }); }

Disabled group

disabled turns off every control in the group at once through the native fieldset, without writing to the children. The disclosure button is exempt, so a disabled group can still be collapsed.

<sui-fieldset disabled collapsible legend="Shipping address, same as billing" style="max-width:460px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="same-street" label="Street" value="410 Pearl Street" style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="same-city" label="City" value="New Albany"></sui-input-text> <sui-input-text size="md" name="same-state" label="State" value="IN"></sui-input-text> <sui-input-text size="md" name="same-zip" label="ZIP" value="47150"></sui-input-text> </div> </sui-fieldset>

In a form

The group is not form associated; the controls inside carry the names and values. A disabled control is barred from submission, so a locked group posts nothing.

Submit to see the collected values.
<form id="fs-form" style="display:flex;flex-direction:column;gap:12px;max-width:460px"> <sui-fieldset legend="Remit to"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> <sui-input-text size="md" name="company" label="Company" value="Falls City Supply Co." style="grid-column:span 2"></sui-input-text> <sui-input-text size="md" name="ein" label="EIN" value="35-1882210"></sui-input-text> <sui-input-text size="md" name="terms" label="Terms" value="Net 30"></sui-input-text> </div> </sui-fieldset> <div style="display:flex;gap:8px;align-items:center"> <sui-button type="submit" variant="brand" size="sm" label="Save remit to"></sui-button> <sui-toggle-switch id="fs-lock" size="sm" label="Lock the group"></sui-toggle-switch> </div> <output id="fs-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see the collected values.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#fs-form'); const out = root.querySelector('#fs-out'); const group = root.querySelector('sui-fieldset'); root.querySelector('#fs-lock').addEventListener('suiChange', (e) => { group.disabled = e.detail; }); form.addEventListener('submit', (e) => { e.preventDefault(); const entries = [...new FormData(form)].map(([k, v]) => `${k}=${v || '(empty)'}`); out.textContent = entries.length ? entries.join(' ') : 'The form collected nothing.'; }); }

Refusing a collapse

The legend emits suiOpen after expanding and a cancelable suiClose with reason trigger before collapsing. Collapsing unmounts the body and takes the typing with it, so a group of fields is the case that most needs the veto.

Idle.
<div style="display:flex;flex-direction:column;gap:10px;max-width:460px"> <sui-fieldset id="fs-events" collapsible legend="Invoice notes"> <sui-textarea rows="2" name="notes" label="Notes" placeholder="Type here, then try to collapse the group"></sui-textarea> </sui-fieldset> <span id="fs-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#fs-log'); const notes = root.querySelector('#fs-events sui-textarea'); const group = root.querySelector('#fs-events'); group.addEventListener('suiClose', (e) => { if (notes.value) { e.preventDefault(); log.textContent = `Collapse refused (${e.detail}). The notes are not saved yet.`; return; } log.textContent = `Collapsed (${e.detail}).`; }); group.addEventListener('suiOpen', () => { log.textContent = 'Expanded.'; }); }

Keyboard

KeyAction
Enter SpaceOn the legend of a collapsible group, toggles the body.

API

<sui-fieldset>

Properties

PropertyAttributeTypeDefaultDescription
collapsedcollapsedbooleanfalseWhether the body is hidden. Seeds the state (the reference defaultCollapsed) and tracks it afterwards; writing it from script emits nothing.
collapsiblecollapsiblebooleanfalsePuts a disclosure button with aria-expanded and aria-controls inside the legend. The body closes over --duration-base and unmounts once it has closed. Reflected.
disableddisabledbooleanfalseDisables every form control inside the group through the native fieldset[disabled] behavior; nothing is written to the children. The disclosure button is exempt (HTML exempts the first legend's descendants), so a disabled group can still be collapsed.
legendlegendstring''Group label shown on the border.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the legend collapses the body; detail is 'trigger' (the legend is the only way this element closes itself). Cancelable: preventDefault() leaves the group open. A group of fields is the case that most needs the veto, because collapsing unmounts the body and takes the user's typing with it. It does not bubble.
suiOpenvoidonSuiOpen@sui-openFires after the legend expands the body. It does not bubble, as no open-state event does.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the disclosure button of a collapsible group. Does nothing on a group that does not collapse, which has no tab stop of its own.

Slots

SlotDescription
(default)Grouped fields.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also