PlAccordion
A stack of sections that fold open one at a time. Use it for reference material a reader scans before choosing what to read, settings groups, specifications, an FAQ.
import { PlAccordion, PlAccordionItem } from 'plass-ui';
<PlAccordion defaultValue={['shipping']}>
<PlAccordionItem value="shipping" title="Shipping">
Three to five working days.
</PlAccordionItem>
<PlAccordionItem value="returns" title="Returns">
Thirty days from delivery.
</PlAccordionItem>
</PlAccordion>;import 'package:plass_ui/plass_ui.dart';
PlAccordion<String>(
value: open,
onChanged: (Set<String> next) => setState(() => open = next),
items: const <PlAccordionItem<String>>[
PlAccordionItem<String>(
value: 'shipping',
title: Text('Shipping'),
child: Text('Three to five working days.'),
),
PlAccordionItem<String>(
value: 'returns',
title: Text('Returns'),
child: Text('Thirty days from delivery.'),
),
],
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | What the sheet is made of. solid is the densest glass, glass is the default sheet with a hairline, ghost has no surface at all |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The type scale of the title and the body, and the padding around both |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| multiple | boolean | false | Lets more than one section stay open at once |
| value | (string | number)[] | — | Which sections are open. Use with onValueChange for a controlled accordion |
| defaultValue | (string | number)[] | — | Which sections start open, for an uncontrolled accordion |
| onValueChange | (value: (string | number)[]) => void | — | Called with the new open set whenever it changes |
| dividers | boolean | true | Scores the sheet between sections with a hairline. Off, each section becomes a tile |
| disabled | boolean | false | Every section stops answering |
| hiddenUntilFound | boolean | false | Keeps closed panels in the DOM so the browser's own page search can find and open them. Overrides keepMounted |
| keepMounted | boolean | false | Keeps closed panels in the DOM. For content that is expensive to build, or that holds form state |
| children | ReactNode | — | The PlAccordionItem sections |
| Prop | Type | Default | Description |
|---|---|---|---|
| items * | List<PlAccordionItem<T>> | — | The sections, as a list of descriptions rather than children — the accordion has to know what is open and where the rules go |
| value * | Set<T> | — | Which sections are open. A set even with multiple off — closed is the empty one |
| onChanged | ValueChanged<Set<T>>? | — | Called with the set that should be open next. Leaving it out freezes the accordion |
| multiple | bool | false | Lets more than one section stay open at once |
| variantshared | PlassVariant | PlassVariant.glass | What the sheet is made of. solid is the densest glass, glass is the default sheet with a hairline, ghost has no surface at all |
| sizeshared | PlassSize | PlassSize.md | The type scale of the title and the body, and the padding around both |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding only — never the height, never the type scale |
| elevationshared | int | 0 | Drop shadow depth. 0 means no shadow at all |
| dividers | bool | true | Scores the sheet between sections with a hairline. Off, each section becomes a tile |
| disabled | bool | false | Every section stops answering |
Every native <div> attribute passes straight through. color is excluded because it collides with the color in the table above, and defaultValue and onChange because the accordion spells them defaultValue (an array) and onValueChange.
The accordion is generic in its section's type (PlAccordion<String>, PlAccordion<Section>), so value and onChanged are typed rather than dynamic, and it is controlled, like every other control in the package. value is a Set<T> even with multiple off, because closed is a set too: an empty one.
PlAccordionItem
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | number | — | Identifies the section to value / defaultValue. Base UI generates one when it is left out |
| title | ReactNode | — | The heading on the fold |
| subtitle | ReactNode | — | A second line under the title, one step down the type scale and muted |
| startIcon | ReactNode | — | Content before the title — an icon, a status dot, a count |
| action | ReactNode | — | A control pinned to the end of the header, before the chevron. It sits outside the trigger, so a button is safe there |
| truncate | boolean | false | Holds the title and the subtitle to one line each, ellipsing what runs past. Off by default, because the heading on a fold is usually a whole sentence |
| disabled | boolean | false | This section stops folding; the rest keep working |
| children | ReactNode | — | The body |
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | T | — | Identifies the section. What PlAccordion.value holds |
| title | Widget? | — | The heading on the fold |
| subtitle | Widget? | — | A second line under the title, one step down the type scale and muted |
| startIcon | Widget? | — | Content before the title — an icon, a status dot, a count |
| action | Widget? | — | A control pinned to the end of the header, before the chevron. It sits outside the fold, so a button is safe there |
| truncate | bool | false | Holds the title and the subtitle to one line each, ellipsing what runs past. Off by default, because the heading on a fold is usually a whole sentence |
| disabled | bool | false | This section stops folding; the rest keep working |
| child | Widget? | — | The body |
size, density and dividers are read from the PlAccordion around the item, not set on it.
A section is a PlAccordionItem, a description rather than a widget. The accordion has to know which sections are open, which one a press should close, and where the rules between them go, and none of that can be asked of an opaque Widget.
It carries no size, no density and no dividers, and could not: those are the accordion's, and a stack with two type scales in it is not one pane.
What the shared axes (variant size color density elevation) mean across the library is in prop conventions.
Examples
variant
The three materials, read the way a container reads them. solid is the clear glass at its most opaque, for a pane that has to sit forward of everything around it. glass is the canonical Plass sheet and the default. ghost has no sheet at all. Reach for it inside a PlCard, where a second bordered rectangle is a second rectangle.
None of the three is dyed. What an accordion holds arrives with its own colours; the family reaches the hover tint, the open section's title and the focus ring, and stops.
multiple
By default opening a section closes the one that was open, which is the whole reason an accordion is not a stack of collapsibles: closing the last as you open the next is what keeps the page from growing under the reader. multiple lifts that.
dividers
On by default: a hairline reaching both edges is what says the folds are parts of one pane. Turn it off and each section becomes its own tile, told apart by space.
title · subtitle · startIcon · action
The title and the subtitle wrap. A fold's heading is most often a whole sentence, an FAQ is a list of questions, and ellipsing one costs the reader the end of it with no tooltip and no way to see it, while wrapping costs a header that is two lines tall in a component whose whole job is to change height. truncate puts each of them back on one line, for a header carrying a name from a database beside a control.
action is rendered outside the fold. A header that both folds and holds a button has two things to press, and one of them cannot be inside the other.
The browser rewrites a <button> inside a <button> on parse, so this is not a preference.
Nothing rewrites the tree here, but a control nested in a control is a press that fires twice and a screen reader reading a button inside a button.
size
Moves the title, the body and the padding around both together. It is set on the accordion and inherited by every section, so a stack cannot end up with two type scales in it.
The body keeps padding of its own above it as well as below. An open header is a tinted band with a bottom edge, and a body that starts at that edge puts its first line half a leading under the title. The heading and the paragraph explaining it read as one run of text broken by a colour change. What the header's padding buys is room around the title; the body buys its own.
Controlled
Pass value with onValueChange to own the open set. Both are arrays even when multiple is off. A closed accordion is [].
There is no uncontrolled mode: value and onChanged are how the accordion is driven, always. value is a Set<T> even when multiple is off, a closed accordion is <String>{}, and leaving onChanged off freezes it at whatever is open, which is how a read-only summary is spelled.
Accessibility
- Each header is a real
<button>carryingaria-expanded, pointed at its panel witharia-controls. The panel is aregionlabelled by its header. - Enter and Space fold a section; Tab moves between headers and into an open panel.
hiddenUntilFoundrenders closed panels withhidden="until-found", so the browser's own page search finds text inside them and opens the section it is in.- The chevron is decorative and
aria-hidden; the open state is carried byaria-expanded, never by the rotation alone. - Anything in
actionis a separate control with its own tab stop, and needs its own accessible name. - The panel animates its height rather than a
transform, so no text is resampled and nothing shifts inside the panel while it opens.
- Each header is announced as a button, expanded or collapsed. The state is carried by that flag, never by the chevron's rotation alone.
- Enter and Space fold a section; Tab moves between headers and into an open panel. Every header is its own focus stop. An accordion is a stack of buttons, not a roving group.
- A closed panel is not in the tree at all, so nothing inside it is reachable, focusable or read out until it is open.
- The chevron is drawn and not named, and a disabled section stops answering both the pointer and the keyboard.
- Anything in
actionis a separate control with its own focus stop, and needs its own name. - The panel animates its height rather than a transform, so no text is resampled and nothing shifts inside the panel while it opens. With animations turned off at the OS it snaps.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
<PlAccordionItem> children | items, as descriptions | The accordion has to know which sections are open, which one a press closes, and where the rules go. None of that can be asked of an opaque widget. |
defaultValue / onValueChange | value / onChanged | Flutter's own controls are controlled, and its name for the callback. |
a value of string | a generic T | Dart has generics, so the section's type is checked rather than restrained by convention. |
value as an array | value as a Set<T> | The open sections are a set (unordered, no duplicates), and Dart has one. |
hiddenUntilFound | — | There is no browser page-search to open a section for. A closed panel simply is not built. |
aria-expanded, aria-controls, region | an expanded button, and a panel that exists or does not | Flutter names the state on the node itself; there is no id to point at. |
children | child | Flutter's name. |
className, style | — | There is no class list and no style attribute to pass through. |