List
A vertical roving-focus list of navigation and action rows, the primitive sidebars compose.
Show codeHide code
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 {
MoreHorizontalOutline,
InboxOutline,
GridLayoutOutline,
SettingsOutline,
} from "@makeplane/propel/icons";
export default function BasicDemo() {
return (
<div className="w-64">
<List role="toolbar" aria-label="Workspace">
<ListItem>
<ListItemLink
href="#inbox"
startIcon={<Icon icon={InboxOutline} size="md" />}
label="Inbox"
/>
</ListItem>
<ListItem>
<ListItemLink
href="#projects"
aria-current="page"
startIcon={<Icon icon={GridLayoutOutline} size="md" />}
label="Projects"
/>
</ListItem>
<ListItem>
<ListItemLink
href="#settings"
startIcon={<Icon icon={SettingsOutline} size="md" />}
label="Settings"
/>
</ListItem>
<ListItem>
<ListItemButton
startIcon={<Icon icon={MoreHorizontalOutline} size="md" />}
label="More"
/>
</ListItem>
</List>
</div>
);
}Examples
Nesting
level (1-5) indents a row to show hierarchy — a workspace containing projects containing pages.
Show codeHide code
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.
Show codeHide code
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.
Show codeHide code
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.
Show codeHide code
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).