Skip to content

PlList

A stack of rows. The list is a sheet and the rows are what is on it, so size and density belong to the stack and a row inherits them.

React
tsx
import { PlList, PlListItem } from 'plass-ui';

<PlList>
  <PlListItem description="Three unread" onClick={open}>
    Inbox
  </PlListItem>
  <PlListItem description="One saved">Drafts</PlListItem>
</PlList>;
dart
import 'package:plass_ui/plass_ui.dart';

PlList(
  children: <Widget>[
    PlListItem(description: const Text('Three unread'), onPressed: open, child: const Text('Inbox')),
    const PlListItem(description: Text('One saved'), child: Text('Drafts')),
  ],
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the sheet is made of. As on any container it is never dyed. ghost is the one to reach for inside a card: the card is already a sheet, and a second bordered rectangle in it is a second rectangle
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Type scale and padding of the rows. A property of the stack, not of any one row in it
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 means no shadow at all
dividersbooleanfalseSeparates the rows with a hairline instead of with space. It changes more than it sounds like: the rules have to reach both edges, so the sheet gives up its padding and the rows give up their corners
renderuseRender.RenderPropRenders something other than a ul — render={<ol />} for a list where the order is the point
childrenReactNodeThe PlListItems
PropTypeDefaultDescription
children * List<Widget>The rows
variantsharedPlassVariantPlassVariant.glassWhat the sheet is made of. As on any container it is never dyed. ghost is the one to reach for inside a card: the card is already a sheet, and a second bordered rectangle in it is a second rectangle
sizesharedPlassSizePlassSize.mdType scale and padding of the rows. A property of the stack, not of any one row in it
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint0Drop shadow depth. 0 means no shadow at all
dividersboolfalseSeparates the rows with a hairline instead of with space. It changes more than it sounds like: the rules have to reach both edges, so the sheet gives up its padding and the rows give up their corners

Every native <ul> attribute passes straight through. color is excluded from the pass-through because it is a Plass prop here.

PlListItem

PropTypeDefaultDescription
onClick(event: MouseEvent) => voidPassing it is what turns the row into a real button. It lands on that button rather than on the li
hrefstringRenders the row as a link
startIconReactNodeContent before the label — an icon, an avatar, a status dot
endIconReactNodeContent after the label, inside the pressable area
descriptionReactNodeA second line under the label, one step down the type scale and muted
actionReactNodeA control pinned to the end of the row. Deliberately **outside** the pressable area: a row that both navigates and holds a toggle has two things to press, and a button inside a button is markup the browser rewrites on parse
selectedbooleanfalseMarks the row as the chosen one — the open page, the current filter. aria-current="page" on a link, "true" on a button
disabledsharedbooleanfalseUnavailable. The light goes out and it stops being pressable
childrenReactNodeThe label
PropTypeDefaultDescription
childWidget?The label
onPressedVoidCallback?Passing it is what turns the row into a real button. It lands on that button rather than on the li
startIconWidget?Content before the label — an icon, an avatar, a status dot
endIconWidget?Content after the label, inside the pressable area
descriptionWidget?A second line under the label, one step down the type scale and muted
actionWidget?A control pinned to the end of the row. Deliberately **outside** the pressable area: a row that both navigates and holds a toggle has two things to press, and a button inside a button is markup the browser rewrites on parse
selectedboolfalseMarks the row as the chosen one — the open page, the current filter. aria-current="page" on a link, "true" on a button
disabledsharedboolfalseUnavailable. The light goes out and it stops being pressable

Every native <li> attribute passes straight through, onto the <li> rather than onto the button or link inside it. size, density and dividers are inherited from the PlList around it, a row that disagreed with its neighbours about any of them is a list with a hole in it.

size, density, color and dividers are inherited from the PlList around it, through an InheritedWidget, a row that disagreed with its neighbours about any of them is a list with a hole in it. Which is also why a PlListItem outside a PlList asserts rather than picking defaults: a row is a row of something.

What the shared axes (variant size color density elevation) mean across the library is in prop conventions.

Examples

A row

The shell is always an <li>. What changes is what is inside it: a plain run of content, or (when onClick or href is given) a real <button> or <a> wrapping that content.

action sits outside that pressable area on purpose. A row that both navigates and holds a toggle has two things to press, and a <button> inside a <button> is markup the browser rewrites on parse.

A row with onPressed is a focus stop announced as a button; one without adds no role and takes none.

action sits outside that pressable area on purpose. A row that both navigates and holds a toggle has two things to press, and a nested gesture recogniser would take one tap twice.

React

dividers

With dividers the rules have to reach both edges of the sheet, so the list gives up its inner padding and the rows give up their rounded corners. A row cannot be a floating tile and a ruled line at the same time.

React

variant

The sheet is never dyed, exactly as on a PlCard. A list holds other people's content, and that content arrives with its own colours.

ghost is the one to use inside a card: the card is already a sheet, and a second bordered rectangle inside it is a second rectangle.

React

size

React

Accessibility

  • There is no Base UI primitive under this on purpose. A list is not a composite widget. It has no roving focus, no selection model, no keyboard contract of its own. Reaching for a menu or a listbox primitive would hand a plain list of links the semantics of a menu.
  • role="list" is written out because Tailwind's reset takes the bullets off every <ul>, and Safari takes the list semantics off with them.
  • A chosen link carries aria-current="page" and a chosen button aria-current="true". The first says "this is the page you are on", the second "this is the chosen one of these". aria-pressed would be a third thing, a toggle, and a selected row is not a toggle.
  • A row with neither onClick nor href adds no role and takes no tab stop. An inert <div> with a click handler on it is invisible to a keyboard.
  • Give the control in action its own accessible name. It is a separate tab stop from the row, which is the point of it being there.
  • A list is not a composite widget (it has no roving focus, no selection model and no keyboard contract of its own), so it adds no role beyond grouping its rows, and each row speaks for itself.
  • A chosen row reports that it is selected. It is not a toggle, and it does not claim to be one.
  • A row with no onPressed adds no role and takes no focus stop.
  • Give the widget in action its own name. It is a separate focus stop from the row, which is the point of it being there.
  • A row's focus ring turns inward when the list is ruled, so it is not sliced off at the sheet's clipped edge.

Differences from the React build

ReactFlutterWhy
onClick / hrefonPressedFlutter has no link element, so a row that navigates calls your router from onPressed.
<ul> / <li> and role="list"a grouped semantics nodeThere are no bullets to reset and no list semantics for a reset to take away.
aria-current="page" vs "true"selectedFlutter's semantics tree has one selection flag and no page-vs-option distinction.
a React contextan InheritedWidgetThe same idea in Flutter's words, for the same reason: cloning children stops reaching a row the moment a caller wraps one.
renderFlutter has no polymorphic element.
children on a rowchildFlutter's name.

Released under the MIT License