Skip to content
Data Display

Item

One row of a menu or list: an icon slot, a truncating label, a trailer, and hover, active and disabled states. A styling primitive: it renders a div until render says otherwise, and then it is whatever you named.

Inbox
Starred
Archive
Trash (emptying)

Import

import {
  Item,
  ItemIcon,
  ItemLabel,
  ItemTrailer,
} from "@mikenotthepope/substrateui"

Composition

ItemIcon is shrink-0 and sizes any nested SVG to 16px; ItemLabel is truncate; ItemTrailer is ms-auto flex-none. Between them that is the whole reason to use the parts rather than raw spans: a long label ellipsises instead of pushing the icon out of the row, and whatever sits at the end stays at the end.

Item
├── ItemIcon
├── ItemLabel
└── ItemTrailer

Rows of actions

A list of things you can do is a list of buttons. Pass one to render and the row becomes it, padding, hover, focus ring and all, rather than becoming a container with a button inside it fighting for the same width.

A bare <button /> is given type="button", because a row of actions standing in a form would otherwise submit it. Write type yourself and yours is kept.

The rule between the rows is divide-y-2 on the list, not border-b on each row. A divider drawn by the parent falls between children and nowhere else, so the list does not end on a rule it then has to take back off with last:border-b-0.

Rows of destinations

The same shape with a different element. Pass an anchor, or your framework's link component, and the row is a link: right-click, middle-click and the status bar all come back.

Size

default is 36px tall at text-sm. That is a menu row: the pointer is already on it, because a menu is what put it there. lg is min-h-11, 44px, the target size of WCAG 2.2 SC 2.5.5, and is what a full-width row that is itself the control should be, because there the row is the only thing to hit and a thumb is what hits it.

Item or ListGroup

Item is one row and nothing around it: no container, no dividers, no keyboard handling. Use it when you are building the surrounding control yourself: the rows of a custom menu, a filter list, a sidebar section, or a list whose parent is already something else, such as a Sortable.

ListGroup is the finished list: a bordered container with separators and rows that take the same render prop. If you want a list, start there; come here when you want to assemble one, or when the list needs no box around it.

Truncation

The label truncates at the row's width, which means the full text is only available on hover or via the accessibility tree. Where the tail of the label matters, give the row a title so the full string is reachable.

Quarterly revenue reconciliation, EMEA, Q3 2026

Accessibility

Without render, Item is a div with cursor-pointer. It looks pressable and is not: no tabIndex, no role, no key handler, so a row carrying only an onClick exists for a mouse user and for nobody else. If the row does something, name the element that does it.

When the row is a control, everything inside it is its accessible name, in source order and run together: adjacent inline spans contribute no whitespace, however far gap-2pushes them apart. A row of an icon, “Salary” and a ItemTrailerreading “Short text” announces as “SalaryShort text”. Keep a marker meaningful (a number, not a bullet), and where a trailer is decoration rather than part of the name, mark it aria-hidden.

A trailer that disappears on a narrow screen disappears from the name too: hidden is display: none. That is usually right, and it is why this component ships no responsive behaviour of its own, because which of two trailers is the expendable one is a fact about the content, not about the row.

active is styling. It sets data-active and no ARIA, so pair it with aria-current on a navigation row or aria-selected in a listbox. Otherwise the current row is only current if you can see the tint.

disabled sets aria-disabled and removes pointer events, which stops the mouse but not the keyboard. A row rendered as a button still needs its own disabled.

API Reference

ItemIcon, ItemLabel and ItemTrailer add no props of their own: everything, including className, goes to the underlying span. Same for anything you pass Item beyond the four below, which reaches whatever element render named.

PropTypeDefaultDescription
renderReact.ReactElement<div />The element the row is. Pass <button /> for a row of actions, an anchor or a framework Link for a row of destinations, <li /> for a row that has to stay a container. The row's classes, data attributes and handlers all land on it.
size"default" | "lg""default"Row height. default is 36px, a menu row under a pointer that is already there. lg is 44px, the target size of WCAG 2.2 SC 2.5.5, for a full-width row that is itself the control.
activebooleanfalseMarks the row as the current one: tinted surface and medium weight. Sets data-active for styling; adds no ARIA.
disabledbooleanfalseDims the row to 50% and removes pointer events. Sets aria-disabled, but cannot disable a control it does not own.