Item

A row in a list: a label, an optional icon anchored to the end, and a selected state. The building block of the sidebar you are reading this in.

API

Props

proptypedefault
as?ElementType"button"the element or component to render
active?booleanfalseselected state; also exposed as data-margo-active
clickable?booleantruewhen false, the row ignores the pointer, the ripple and the tab order
disabled?booleanfalseunavailable: inert, dimmed, aria-disabled and out of the tab order
onClick?MouseEventHandlercalled on click, before onClickBlur
onClickBlur?MouseEventHandlercalled on click, then the row loses focus 150ms later
…restprops of astyped against the chosen element

Item.Icon

proptypedefault
icon?ReactNode<MdChevronRight />the glyph; the default is the chevron
className?stringmerged over the defaults

Mounting it is what makes it exist: an Item without Item.Icon has no chevron and no space reserved for one. The icon prop is a pure override, so the default chevron costs nothing to keep and one prop to replace.

Item.Label

proptypedefault
labelReactNodethe row text; truncates rather than wrapping
className?stringmerged over the defaults

The label truncates instead of wrapping: a sidebar row that grows to two lines breaks the rhythm of the list far more than a clipped word does. That holds for a navigation list, not for every list — a row carrying a message or a file name may well want two lines, and the class is merged, so it takes one className.

Examples

Usage

An Item is a full width row: whatever you mount flows from the left, and the icon slot anchors to the right. At rest it is quiet — muted text, no border — and it takes a border on hover and the primary gradient when active.

tsx
<Item as={Link} to="/margo-ui/item" active={isActive} onClickBlur={() => {}}>
  <Item.Label label="Item" />
  <Item.Icon />
</Item>
previewhover the rows

Free content, anchored slots

The row does not assume a label and nothing else. Anything can go inside — a chip, a count, a second line — and the icon still ends up at the end, because the slot carries the auto margin rather than the label carrying a flex grow.

tsx
<Item>
  <Item.Label label="typescript" />
  <Chip>4</Chip>
  <Item.Icon icon={<MdOpenInNew />} />
</Item>
preview
tsx
<Item>
  <Item.Icon icon={<MdChevronLeft />} />
  <Item.Label label="back" />
</Item>
tsx
export const itemIconClassName = `ml-auto flex size-4 shrink-0 items-center justify-center first:ml-0
[[data-margo-item-slot]+&]:ml-0`;

States

active paints the row with the primary gradient. clickable set to false drops pointer events, the ripple and the tab stop while leaving the row looking untouched — the shape of a row waiting on something. disabled does the same and dims it, so the row reads as unavailable rather than merely busy.

tsx
<Item active></Item>
<Item clickable={false}></Item>
<Item disabled></Item>
previewneither of the last two responds

Details

Active

active paints the row with the primary gradient border and its elevation, and is also reflected as data-margo-active on the element. That is what lets the parts style themselves from the state without it being passed down to them.

As everywhere in the kit, it is visual only. A navigation row should carry aria-current="page" as well, which is exactly what the sidebar of this site does. disabled is the exception: it sets aria-disabled itself, because an unavailable row that only looks unavailable is a trap for anyone not seeing it.

Item or Button

Both render a button by default, because both are controls: a row you click is a button that happens to be shaped like a row, and the cursor and the keyboard should say so. A Button is a control that compresses to its icon and expands on demand; an Item is a row in a list that always shows its label. They also differ in what they occupy: a Button is w-fit, an Item is full width by construction, because a list of rows of different widths is not a list.

When a row needs two lines

tsx
<Item.Label label={name} className="line-clamp-2" />

<Item.Label label={name} className="whitespace-normal" />

How the anchoring works

Two slots in a row would normally split the free space between them and drift apart. Both are marked with a data attribute, and a slot that follows another drops its auto margin — so the first one anchors and the rest queue up beside it, in CSS, with no measuring.

A slot mounted before anything else drops that margin too, which is how an icon leads the row instead of trailing it: on a first child the automatic margin has nothing to push against and would eat the free space on its left, dragging the label with it. Mount Item.Icon first and it stays at the start — no reversed direction, no override at the call site.