Skip to content

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.

React
tsx
import { PlCollapsible } from 'plass-ui';

<PlCollapsible title="Advanced" subtitle="Nine settings">
  Everything the form does not need to ask on the first pass.
</PlCollapsible>;
dart
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

PropTypeDefaultDescription
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
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 is the default: a fold is set into the page, not floating over it
openbooleanWhether the panel is showing. Use with onOpenChange for a controlled fold
defaultOpenbooleanfalseWhere an uncontrolled fold starts
onOpenChange(open: boolean) => voidCalled when the trigger opens or closes the panel
titleReactNodeThe heading on the trigger
subtitleReactNodeA second line under the title, one step down the type scale and muted
startIconReactNodeContent before the title — an icon, a status dot, a count
actionReactNodeA control pinned to the end of the header, **outside** the trigger: a button inside a button is markup the browser rewrites
triggerReactElementReplaces the header entirely with a control of your own. The element you pass becomes the trigger
indicatorbooleantrueThe chevron at the end of the header, turned to report the state
disabledbooleanfalseThe trigger stops answering and the panel stays as it is
paddedbooleantrueInner padding around the panel's content
hiddenUntilFoundbooleanfalseKeeps a closed panel in the DOM so the browser's own page search can find and open it. Overrides keepMounted
keepMountedbooleanfalseKeeps a closed panel in the DOM. For content that is expensive to build, or that holds form state
PropTypeDefaultDescription
open * boolWhether the panel is showing. Use with onOpenChange for a controlled fold
onOpenChangedValueChanged<bool>?Called with what the open state should become
childWidget?The body
titleWidget?The heading on the trigger
subtitleWidget?A second line under the title, one step down the type scale and muted
startIconWidget?Content before the title — an icon, a status dot, a count
actionWidget?A control pinned to the end of the header, **outside** the trigger: a button inside a button is markup the browser rewrites
triggerBuilderWidget 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
indicatorbooltrueThe chevron at the end of the header, turned to report the state
disabledboolfalseThe trigger stops answering and the panel stays as it is
paddedbooltrueInner padding around the panel's content
keepMountedboolfalseKeeps 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
variantsharedPlassVariantPlassVariant.glassWhat the sheet is made of. Never dyed — a fold holds other people's content. ghost is the one for running prose or a card
sizesharedPlassSizePlassSize.mdThe radius, the padding and the header's type scale
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint0Drop 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.

React

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.

React

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.

React

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:

  • hiddenUntilFound keeps it there as hidden="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.
  • keepMounted keeps 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.
  • action is 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 the aria-expanded / aria-controls wiring between it and the panel.
  • With hiddenUntilFound the browser's find-in-page opens the fold it found the text in, rather than scrolling to nothing.
  • With keepMounted the 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

ReactFlutterWhy
open / defaultOpen / onOpenChangeopen / onOpenChangedFlutter's own controls are controlled, and so is every stateful widget in this package.
trigger, an elementtriggerBuilder, a builderA 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.
hiddenUntilFoundThere is no browser find-in-page to open a fold from.
keepMounted keeps a hidden panel in the DOMkeepMounted keeps it in the treeThe 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, styleThere is no class list and no style attribute to pass through.

Released under the MIT License