PlCollapsible
One section that folds, standing on its own. The same fold a PlAccordion is a set of, with nothing else beside it, so what it needs is an open of its own rather than a place in somebody's list.
import { PlCollapsible } from 'plass-ui';
<PlCollapsible title="Advanced" subtitle="Nine settings">
Everything the form does not need to ask on the first pass.
</PlCollapsible>;import 'package:plass_ui/plass_ui.dart';
PlCollapsible(
open: showing,
onOpenChanged: (bool next) => setState(() => showing = next),
title: const Text('Advanced'),
subtitle: const Text('Nine settings'),
child: const Text('Everything the form does not need to ask on the first pass.'),
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | What the sheet is made of. Never dyed — a fold holds other people's content. ghost is the one for running prose or a card |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The radius, the padding and the header's type scale |
| 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 is the default: a fold is set into the page, not floating over it |
| open | boolean | — | Whether the panel is showing. Use with onOpenChange for a controlled fold |
| defaultOpen | boolean | false | Where an uncontrolled fold starts |
| onOpenChange | (open: boolean) => void | — | Called when the trigger opens or closes the panel |
| title | ReactNode | — | The heading on the trigger |
| 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, **outside** the trigger: a button inside a button is markup the browser rewrites |
| trigger | ReactElement | — | Replaces the header entirely with a control of your own. The element you pass becomes the trigger |
| indicator | boolean | true | The chevron at the end of the header, turned to report the state |
| disabled | boolean | false | The trigger stops answering and the panel stays as it is |
| padded | boolean | true | Inner padding around the panel's content |
| hiddenUntilFound | boolean | false | Keeps a closed panel in the DOM so the browser's own page search can find and open it. Overrides keepMounted |
| keepMounted | boolean | false | Keeps a closed panel in the DOM. For content that is expensive to build, or that holds form state |
| Prop | Type | Default | Description |
|---|---|---|---|
| open * | bool | — | Whether the panel is showing. Use with onOpenChange for a controlled fold |
| onOpenChanged | ValueChanged<bool>? | — | Called with what the open state should become |
| child | Widget? | — | The body |
| title | Widget? | — | The heading on the trigger |
| 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, **outside** the trigger: a button inside a button is markup the browser rewrites |
| triggerBuilder | Widget Function(BuildContext, bool open, VoidCallback toggle)? | — | Replaces the header entirely. A builder rather than a widget: a Dart widget cannot be handed a tap handler after it was made |
| indicator | bool | true | The chevron at the end of the header, turned to report the state |
| disabled | bool | false | The trigger stops answering and the panel stays as it is |
| padded | bool | true | Inner padding around the panel's content |
| keepMounted | bool | false | Keeps a closed panel in the tree: a State goes with its widget, so a folded-away field forgets what was typed. It leaves the focus order and the semantics tree while it is closed |
| variantshared | PlassVariant | PlassVariant.glass | What the sheet is made of. Never dyed — a fold holds other people's content. ghost is the one for running prose or a card |
| sizeshared | PlassSize | PlassSize.md | The radius, the padding and the header's type scale |
| 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 is the default: a fold is set into the page, not floating over it |
Every other <div> attribute passes through to the sheet.
What the shared axes mean across the library is in prop conventions.
PlCollapsible or PlAccordion
A PlAccordion is a set, and the set is the point: closing the last section as the next opens is what keeps the page from growing under the reader. A collapsible has nobody to coordinate with.
Reach for this for a "Show more" on a form, an optional block of settings, the details under a row. Reach for an accordion the moment there are two of them and only one should be open at a time.
How the panel opens
The panel's height is animated, which looks like an exception to the rule against moving things and is not: nothing is transformed, no text is resampled, and the content does not shift relative to the panel it is in. The panel is a window opening onto it.
Content that appears instantly is a page that jumps, which is the failure the rule exists to prevent.
Examples
variant
The three materials, read as a container's: the sheet is never dyed, because a fold holds other people's content. ghost is the one to use inside running prose or inside a card, a bare "Show more" line owes the page no rectangle of its own.
The header's slots
title, subtitle and startIcon are the header. action is pinned to the end of it and sits outside the trigger, which is not a layout preference: a header that both folds and holds a switch has two things to press, and one of them cannot be nested inside the other.
The chevron is turned rather than moved, and it is the only thing on the header that reports the state by moving, which is why the header itself only changes colour.
trigger
Replaces the header entirely with a control of your own.
title and the slots around it are for the far commoner case of wanting the header that is already there.
The element you pass becomes the trigger: it is handed the click handler, aria-expanded and the aria-controls pointing at the panel, so nothing has to be wired up.
triggerBuilder is a builder rather than a widget, and that is forced: a React element can be cloned with new props, and a Dart widget cannot be handed a tap handler after it was made. So the builder is given the open state and the callback and wires up whatever it likes.
hiddenUntilFound and keepMounted
A closed panel is not in the document, which is what makes an unopened fold cost nothing. Two props take that back, for two different reasons:
hiddenUntilFoundkeeps it there ashidden="until-found", so the browser's own page search can find the text inside a closed fold and open it. That is the one worth using on a documentation page.keepMountedkeeps it there outright, for content that is expensive to build or that holds form state which should survive being folded away.
hiddenUntilFound overrides keepMounted; it is the same idea with the browser's find-in-page bolted on.
Accessibility
- The header is announced as a button, reports whether the panel is open, and answers a press from the keyboard as readily as from a pointer.
actionis outside the trigger, so it is its own focus stop rather than a control nested inside another one.- A disabled fold's trigger is out of the focus order, and the panel stays exactly as it was.
- The header is a real
<button>and Base UI owns thearia-expanded/aria-controlswiring between it and the panel. - With
hiddenUntilFoundthe browser's find-in-page opens the fold it found the text in, rather than scrolling to nothing.
- With
keepMountedthe closed panel is clipped to nothing and taken out of the focus order and off the semantics tree: a panel nobody can see is not one a keyboard should be able to tab into.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
open / defaultOpen / onOpenChange | open / onOpenChanged | Flutter's own controls are controlled, and so is every stateful widget in this package. |
trigger, an element | triggerBuilder, a builder | A React element can be cloned with new props; a Dart widget cannot be handed a tap handler after it was made. The builder gets the state and the callback instead. |
hiddenUntilFound | — | There is no browser find-in-page to open a fold from. |
keepMounted keeps a hidden panel in the DOM | keepMounted keeps it in the tree | The same idea and a sharper reason: a Flutter State goes with its widget when it leaves the tree, so a folded-away field forgets what was typed into it. |
className, style | — | There is no class list and no style attribute to pass through. |