List
A stack of rows: navigation, settings, results, anything that repeats.
import { List, ListItem } from 'neba';
<List>
<ListItem startIcon={<GlobeIcon />} description="Deployed 4 minutes ago" onClick={open} selected>
production
</ListItem>
<ListItem startIcon={<GlobeIcon />} description="Deployed 2 hours ago" onClick={open}>
staging
</ListItem>
</List>;List
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. The sheet is never dyed, as on Box. Reach for text inside a Card — the card is already a sheet |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The rows' type scale and padding. An axis of the list, not of any one row |
| 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 means no shadow at all |
| dividers | boolean | false | Separates the rows with a hairline instead of space. It changes more than it sounds like: the rules have to reach both edges, so the list gives up its inner padding and the rows give up their corners |
| render | useRender.RenderProp | — | Renders something other than a ul — render={<ol />} when the order is the point |
| children | ReactNode | — | The ListItems |
ListItem
| Prop | Type | Default | Description |
|---|---|---|---|
| startIcon | ReactNode | — | Content before the label — an icon, an avatar, a status dot |
| endIcon | ReactNode | — | Content after the label, inside the pressable area |
| description | ReactNode | — | A second line under the label |
| action | ReactNode | — | A control pinned to the end of the row. Deliberately outside the pressable area — a button inside a button is markup the browser rewrites on parse |
| onClick | (event) => void | — | Passing it is what turns the row into a real button |
| href | string | — | Passing it is what turns the row into a real link |
| selected | boolean | false | The chosen row — the open page, the current filter. A link gets aria-current="page" and a button aria-current="true" |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
| children | ReactNode | — | The label |
Examples
Tiles or rules
dividers changes more than it sounds like. With the rules on, the lines 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.
Inside a Card
variant="text" is the one to reach for there. The card is already a sheet, and a second bordered rectangle inside it is a rectangle too many.
A row with a control on it
action sits outside the pressable area on purpose. A row that both navigates and holds a switch has two things to press, and one of them cannot be nested inside the other — a <button> inside a <button> is markup the browser rewrites on parse.
Two components, one axis each
size and density are properties of the stack, not of any one line in it, so they live on List and reach the rows through a context. Setting them per row would be two chances per row to get one wrong, and the failure is silent: a list where item four is a size bigger than the rest.
A context rather than React.Children.map with cloneElement, for the same reason ButtonGroup uses one — the moment a caller .map()s their data or wraps a row in a Tooltip, cloning stops reaching the item.
No primitive underneath
There is no Base UI component 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 to get one would hand every consumer's plain list of links the semantics of a menu, which is one of the most common ways a component library breaks a screen reader.
Accessibility
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 it.
selected puts aria-current="page" on a link and aria-current="true" on a button. Not aria-pressed, which would be a third thing: a toggle. A selected row is not a toggle.
The list says role="list" out loud, because Tailwind's reset takes the bullets off every <ul> and Safari takes the list semantics off with them.