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
| prop | type | default | |
|---|---|---|---|
as? | ElementType | "button" | the element or component to render |
active? | boolean | false | selected state; also exposed as data-margo-active |
clickable? | boolean | true | when false, the row ignores the pointer, the ripple and the tab order |
disabled? | boolean | false | unavailable: inert, dimmed, aria-disabled and out of the tab order |
onClick? | MouseEventHandler | — | called on click, before onClickBlur |
onClickBlur? | MouseEventHandler | — | called on click, then the row loses focus 150ms later |
…rest | props of as | — | typed against the chosen element |
Item.Icon
| prop | type | default | |
|---|---|---|---|
icon? | ReactNode | <MdChevronRight /> | the glyph; the default is the chevron |
className? | string | — | merged 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
| prop | type | default | |
|---|---|---|---|
label | ReactNode | — | the row text; truncates rather than wrapping |
className? | string | — | merged 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.
<Item as={Link} to="/margo-ui/item" active={isActive} onClickBlur={() => {}}>
<Item.Label label="Item" />
<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.
<Item>
<Item.Label label="typescript" />
<Chip>4</Chip>
<Item.Icon icon={<MdOpenInNew />} />
</Item><Item>
<Item.Icon icon={<MdChevronLeft />} />
<Item.Label label="back" />
</Item>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.
<Item active>…</Item>
<Item clickable={false}>…</Item>
<Item disabled>…</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, 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
<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.