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 is inert: no pointer, no ripple, no tab stop
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

Item.Label

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

Variables

token
--margo-shadow-itemits elevation on hover, focus and active

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 onClickBlur={handleSelect}>
  <Item.Label label="Introduction" />
  <Item.Icon />
</Item>

<Item active onClickBlur={handleSelect}>
  <Item.Label label="foundation" />
  <Item.Icon />
</Item>

<Item onClickBlur={handleSelect}>
  <Item.Label label="margo grid" />
  <Item.Icon />
</Item>

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 className="mx-2">4</Chip>
  <Item.Icon icon={<MdOpenInNew />} />
</Item>

<Item>
  <Item.Icon icon={<MdStar />} />
  <Item.Label label="starred" />
  <Item.Icon />
</Item>
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 marks the row inert — no pointer, no ripple, no tab stop — while leaving it 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 onClickBlur={handleSelect}>
  <Item.Label label="active" />
  <Item.Icon />
</Item>

<Item clickable={false}>
  <Item.Label label="inert" />
  <Item.Icon />
</Item>

<Item disabled>
  <Item.Label label="disabled" />
  <Item.Icon />
</Item>

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 and marks the row inert, so it is not merely dimmed but out of reach for pointer, keyboard and assistive technology alike.

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.

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.