List
A vertical roving-focus list of navigation and action rows, the primitive sidebars compose.
Installation
import {
List,
ListItem,
ListItemButton,
ListItemCounter,
ListItemDisclosureTrigger,
ListItemLink,
ListSection,
ListSectionHeading,
} from "@makeplane/propel/components/list";Usage
import { Icon } from "@makeplane/propel/components/icon";
import { List, ListItem, ListItemButton, ListItemLink } from "@makeplane/propel/components/list";
import { Ellipsis, Inbox, LayoutGrid, Settings } from "lucide-react";
export default function BasicDemo() {
return (
<div className="w-64">
<List role="toolbar" aria-label="Workspace">
<ListItem>
<ListItemLink href="#inbox" startIcon={<Icon icon={Inbox} size="md" />} label="Inbox" />
</ListItem>
<ListItem>
<ListItemLink
href="#projects"
aria-current="page"
startIcon={<Icon icon={LayoutGrid} size="md" />}
label="Projects"
/>
</ListItem>
<ListItem>
<ListItemLink
href="#settings"
startIcon={<Icon icon={Settings} size="md" />}
label="Settings"
/>
</ListItem>
<ListItem>
<ListItemButton startIcon={<Icon icon={Ellipsis} size="md" />} label="More" />
</ListItem>
</List>
</div>
);
}Examples
Nesting
level (1-5) indents a row to show hierarchy — a workspace containing projects containing pages.
Text density
density sets the row’s label/counter text scale: comfortable (the default, 14px) or compact (13px). Row height and icon size stay fixed either way — this is not a ladder size (those labels map to control heights).
Show codeHide code
Row disclosure
ListItemDisclosureTrigger is a row’s own expand/collapse control — a sibling of the row’s primary, never nested inside it, since a button can’t nest inside a link or button. It is a roving CompositeItem in the parent List (one tab stop with the primary; arrow keys move between them).
Unlike ListSection (which wraps Base UI Collapsible for you), there is no ready-made row collapsible. Compose it yourself with Collapsible.Root from @base-ui/react/collapsible:
- Give each expandable row its own
Collapsible.Root. - Inside the root: a parent
Listthat holds only that expandable row (primary + disclosure trigger). - Put
Collapsible.Panelnext to that parentList, still inside the same root — same shapeListSectionuses, scoped to one row. Wrap the nested childListinListSectionPanelContent(heading-to-row gap only). Do not useCollapsiblePanelContenthere — its inset and body-xs prose are for Collapsible’s own panel body; list rows already own their padding. - Sibling top-level rows that are not children of the expandable row go in a separate
Listoutside that root (after it in the stack). Do not put those siblings in the same parentListas the expandable row: the panel is a sibling of the list, so it would render after every list item and break visual hierarchy.
This example shows count and endIcon on the expandable row, nested children under it, then a sibling Settings row after the cluster.
Trailing order on a row: count → endIcon → disclosure trigger. count and endIcon live inside the primary (ListItemLink / ListItemButton) — they are visual only; clicks still hit the link/button. Interactive trailing controls (disclosure, a “more” menu, etc.) must be siblings of the primary, never passed as endIcon. The row’s full focus ring only follows the primary; sibling controls keep their own focus chrome.
A bare count (e.g. 6) only tells a screen reader “Inbox, 6” — no indication of what the number means, since the component itself can’t know (unread messages? total items? something else?). If that distinction matters for your use case, add it yourself with visually-hidden text — count accepts any node, not just a string or number:
<ListItemLink
href="#inbox"
label="Inbox"
count={
<>
<span className="sr-only">unread: </span>6
</>
}
/>Section
Wrap a List in ListSection to get a muted heading that collapses its body. The disclosure chevron rotates as the section opens and closes.
Section heading
For settings-style sidebars whose groups never collapse, ListSectionHeading titles a List with a static, non-interactive label.
Show codeHide code
Controlled
Drive a section’s open state yourself with open and onOpenChange.
API Reference
List itself forwards Base UI’s Composite props and the underlying list element’s HTML attributes — one tab stop for the whole list, arrow keys move between rows. Pass the role/aria-* your context calls for.
ListItem
A row wrapper. Holds a primary `ListItemLink` (or `ListItemButton`) plus optional actions or a count as siblings, and carries the row chrome. `level` indents nested rows; `density` sets the label/counter text scale. Renders a `<div>` by default.
ListItemLink
A list row's primary navigation target — an `<a>` that is also a roving `Composite` item. Base UI's `CompositeItem` roving behavior grafted onto the styled `elements` `ListItemLink` element (rule 1a). Mark the current page with `aria-current="page"`. Pass `startIcon` for the leading visual, `label` for the visible row text, `count` for a trailing count chip, and `endIcon` for a trailing visual. A row's expand/collapse control (`ListItemDisclosureTrigger`) is a separate sibling, never a child — a button can't nest inside this `<a>`.
ListItemButton
A list row's primary action — a `<button>` that is also a roving `Composite` item. Base UI's `CompositeItem` roving behavior grafted onto the styled `elements` `ListItemButton` element (rule 1a). Pass `startIcon` for the leading visual, `label` for the visible row text, `count` for a trailing count chip, and `endIcon` for a trailing visual. A row's expand/collapse control (`ListItemDisclosureTrigger`) is a separate sibling, never a child — a button can't nest inside this `<button>`.
ListItemCounter
The row's trailing count chip (e.g. an unread count).
ListItemDisclosureTrigger
A row's expand/collapse control — Base UI `Collapsible.Trigger` grafted onto the styled trailing-edge element (rule 1a), with the shared rotating caret baked in, and registered as a roving `CompositeItem` so it participates in the parent `List`'s one-tab-stop / arrow-key contract (same as `ListItemLink` / `ListItemButton`). Compose as a sibling of the row's primary inside a `ListItem`, itself inside the `Collapsible.Root` that also wraps the nested `List` panel — same shape as `ListSection`, just scoped to one row instead of a whole heading.
ListSection
A ready-made collapsible list section: a muted heading that toggles its body, with the disclosure chevron (points inline-end while collapsed, rotates down when open). Pass `label` for the heading and `children` for the body (typically a `List` of rows); forward `defaultOpen` (uncontrolled) or `open` + `onOpenChange` (controlled) to drive it. Set `indicator={false}` to omit the chevron. Children sit in `ListSectionPanelContent` (heading-to-row gap only) inside the height-animating panel — list rows own their own horizontal padding, so this does not wrap `CollapsiblePanelContent` (that part carries Collapsible's Figma panel inset + prose).