V2 componentsUpdated Oct 14, 2026
Item
A settings row: what it is, what it means, and the one control that changes it. Use it for any title + description + trailing control, stacked in an Item.Group with hairlines between. Not a Cell, which is a raised card you press, and not a Field, which wires a label to an input: when the control is an input use a justified Field, which shares Item’s type and sits in the same group.
Source: packages/ui/src/components/item
import { Item, itemVariants, itemTitleClasses, itemDescriptionVariants } from '@highlight/ui'Change the props; the preview and the snippet update together.
<Item> <Item.Content> <Item.Title>Export data</Item.Title> <Item.Description> Copy this workspace’s data to files or to another workspace. </Item.Description> </Item.Content> <Item.Actions><Button variant="secondary" size="sm">Export</Button></Item.Actions></Item>Content fills; Actions sizes to itself at the far end. The title truncates, the description clamps to two lines.
import { Button, Item } from '@highlight/ui' export default function ItemBasic() { return ( <Item className="max-w-xl"> <Item.Content> <Item.Title>Export data</Item.Title> <Item.Description>Copy this workspace’s data to files or to another workspace.</Item.Description> </Item.Content> <Item.Actions> <Button variant="secondary" size="sm"> Export </Button> </Item.Actions> </Item> )}Item.Group draws the hairline between rows and sets their density. Items pad themselves; a justified Field in the same group takes the group’s padding so it lines up.
import { ExternalLink } from 'lucide-react'import { Button, Item } from '@highlight/ui' export default function ItemGroup() { return ( <Item.Group className="max-w-xl"> <Item> <Item.Content> <Item.Title>Single sign-on</Item.Title> <Item.Description>Members sign in through your identity provider.</Item.Description> </Item.Content> <Item.Actions> <Button variant="secondary" size="sm" right={<ExternalLink />}> Configure </Button> </Item.Actions> </Item> <Item> <Item.Content> <Item.Title>Directory sync</Item.Title> <Item.Description> People removed from your identity provider are removed from this workspace. </Item.Description> </Item.Content> <Item.Actions> <Button variant="secondary" size="sm" right={<ExternalLink />}> Set up </Button> </Item.Actions> </Item> </Item.Group> )}separator={false} drops the hairlines. For a list the eye already parses row by row, such as one with media tiles, the lines are noise. Pair it with size="sm" for a denser list.
import { Calendar, FileText, Mic } from 'lucide-react'import { Button, Item } from '@highlight/ui' export default function ItemNoSeparator() { return ( <Item.Group separator={false} size="sm" className="max-w-xl"> <Item> <Item.Media variant="icon"> <Mic /> </Item.Media> <Item.Content> <Item.Title>Weekly sync</Item.Title> <Item.Description>Recorded 42 min ago</Item.Description> </Item.Content> <Item.Actions> <Button variant="ghost" size="sm"> Open </Button> </Item.Actions> </Item> <Item> <Item.Media variant="icon"> <FileText /> </Item.Media> <Item.Content> <Item.Title>Design review notes</Item.Title> <Item.Description>Edited yesterday</Item.Description> </Item.Content> <Item.Actions> <Button variant="ghost" size="sm"> Open </Button> </Item.Actions> </Item> <Item> <Item.Media variant="icon"> <Calendar /> </Item.Media> <Item.Content> <Item.Title>Q4 planning</Item.Title> <Item.Description>Tomorrow, 10:00</Item.Description> </Item.Content> <Item.Actions> <Button variant="ghost" size="sm"> Open </Button> </Item.Actions> </Item> </Item.Group> )}image is a bordered tile for a brand mark, icon a quiet tile for a lucide glyph, avatar a circle for a person. Media is aria-hidden: the title names the row.
import { Building2, Plus } from 'lucide-react'import { Button, Item } from '@highlight/ui' export default function ItemMedia() { return ( <Item.Group className="max-w-xl"> <Item> <Item.Media variant="image"> <Building2 /> </Item.Media> <Item.Content> <Item.Title>Google Workspace</Item.Title> <Item.Description>Approve Highlight for your whole company.</Item.Description> </Item.Content> <Item.Actions> <Button variant="outline" shape="square" aria-label="Connect Google Workspace"> <Plus /> </Button> </Item.Actions> </Item> <Item> <Item.Media variant="icon"> <Building2 /> </Item.Media> <Item.Content> <Item.Title>Ashby</Item.Title> <Item.Description>One API key signs in every member.</Item.Description> </Item.Content> <Item.Actions> <Button variant="outline" shape="square" aria-label="Connect Ashby"> <Plus /> </Button> </Item.Actions> </Item> <Item> <Item.Media variant="avatar"> <img alt="" src="https://i.pravatar.cc/64?img=12" /> </Item.Media> <Item.Content> <Item.Title>Maya Chen</Item.Title> <Item.Description>Admin</Item.Description> </Item.Content> </Item> </Item.Group> )}render swaps the element. An anchor or a button makes the whole row the target. The hover wash sits behind the row and bleeds past its edges, so the group’s hairlines stay straight.
import { ChevronRight } from 'lucide-react'import { Item } from '@highlight/ui' export default function ItemLink() { return ( <Item.Group className="max-w-xl"> <Item render={<a href="#members" />}> <Item.Content> <Item.Title>Members</Item.Title> <Item.Description>Who is in this workspace and what they can do.</Item.Description> </Item.Content> <Item.Actions> <ChevronRight className="text-foreground-secondary size-4" /> </Item.Actions> </Item> <Item render={<button type="button" onClick={() => alert('Pressed')} />}> <Item.Content> <Item.Title>Billing</Item.Title> <Item.Description>Plan, seats and invoices.</Item.Description> </Item.Content> <Item.Actions> <ChevronRight className="text-foreground-secondary size-4" /> </Item.Actions> </Item> </Item.Group> )}A justified Field in the same group. Field.Label and Field.Note are built from Item’s title and description styles, so the two rows read as one list.
Shown to members at sign-in.
import { Button, Field, Input, Item } from '@highlight/ui' export default function ItemWithField() { return ( <Item.Group className="max-w-xl"> <Item> <Item.Content> <Item.Title>Logo</Item.Title> <Item.Description>Square PNG or SVG, at least 256 × 256 px.</Item.Description> </Item.Content> <Item.Actions> <Button variant="secondary" size="sm"> Upload </Button> </Item.Actions> </Item> <Field orientation="justified"> <Field.Label>Workspace name</Field.Label> <Field.Note>Shown to members at sign-in.</Field.Note> <Input defaultValue="Maya’s workspace" className="w-60" /> </Field> </Item.Group> )}disabled greys the text and leaves the control to carry its own disabled. Description tone="danger" is the error in place of the explanation, announced, with no layout shift.
import { Badge, Button, Item } from '@highlight/ui' export default function ItemStates() { return ( <Item.Group className="max-w-xl"> <Item disabled> <Item.Content> <Item.Title>Leave Maya’s workspace</Item.Title> <Item.Description>You’re the only one here. Deactivate it instead.</Item.Description> </Item.Content> <Item.Actions> <Button variant="secondary" tone="danger" size="sm" disabled> Leave workspace </Button> </Item.Actions> </Item> <Item> <Item.Content> <Item.Title>Deactivate Maya’s workspace</Item.Title> <Item.Description>Everyone loses access. Data is deleted after 30 days.</Item.Description> </Item.Content> <Item.Actions> <Button variant="secondary" tone="danger" size="sm"> Deactivate </Button> </Item.Actions> </Item> <Item> <Item.Content> <Item.Title badge={<Badge tone="danger">Not verified</Badge>}>Google Workspace</Item.Title> <Item.Description tone="danger">Finish authorizing Highlight in the Google Admin console.</Item.Description> </Item.Content> <Item.Actions> <Button variant="secondary" size="sm"> Verify </Button> </Item.Actions> </Item> </Item.Group> )}default is flat for lists. outline and muted give a lone row its own edge or wash.
import { Button, Item } from '@highlight/ui' export default function ItemVariants() { return ( <div className="flex max-w-xl flex-col gap-4"> <Item variant="outline"> <Item.Content> <Item.Title>Outline</Item.Title> <Item.Description>Its own edge, for a row that stands alone.</Item.Description> </Item.Content> <Item.Actions> <Button variant="ghost" size="sm"> Action </Button> </Item.Actions> </Item> <Item variant="muted"> <Item.Content> <Item.Title>Muted</Item.Title> <Item.Description>A quiet wash, for a row that is informational.</Item.Description> </Item.Content> </Item> <Item size="sm" variant="outline"> <Item.Content> <Item.Title>Small</Item.Title> <Item.Description>Tighter padding for dense lists.</Item.Description> </Item.Content> </Item> </div> )}The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.
Item- A div holding one row: Item.Content beside Item.Actions. variant and size set its look.
Item.Group- Stacks rows with a hairline between each; accepts Items and justified Fields. size sets the density of every Item in it; separator={false} drops the hairlines.
Item.Media- Leading tile. variant: icon | image | avatar.
Item.Content- The elastic middle holding Title and Description.
Item.Title- One line, truncates. badge puts a Badge after the text.
Item.Description- Two lines, clamped. tone="danger" turns it into the announced error.
Item.Actions- Trailing controls, sized to themselves.
Props added on top of the underlying element or Base UI part.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default'|'outline'|'muted' | 'default' | default is flat, for rows in a group. outline draws a border, muted a subtle wash, for a row on its own. |
size | 'md'|'sm' | 'md' | Vertical padding and gap: 12 / 8. Inherits from a surrounding Item.Group when omitted. |
align | 'center'|'start' | 'center' | start pins media and actions to the first line when the description runs long beside a tall control. |
render | ReactElement | — | The element to render as, e.g. <a href> or <button>. A rendered row is interactive: hover wash and focus ring. |
disabled | boolean | false | Greys the title and description. |
- itemTitleClasses and itemDescriptionVariants are the type Field.Label and Field.Note are built from. Import them for a one-off row rather than restating the classes.
- Media is aria-hidden by default. Pass aria-hidden={false} and an aria-label when the picture carries something the title does not.