Item
The generic dense row — a media gutter, a content column, and the actions against the trailing edge.
import { Item, ItemGroup, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription, ItemActions } from "@glint/components/item.slint";import { Button, ButtonVariant, ButtonSize } from "@glint/components/button.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 640px; height: 300px; background: Tokens.color-background;
VerticalLayout { padding: 24px; alignment: start;
ItemGroup { label: "Integrations"; item-count: 3;
Item { index: 0; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.GitBranch; } ItemContent { ItemTitle { text: "Source control"; } ItemDescription { text: "Pull requests and issues."; } } ItemActions { Button { variant: ButtonVariant.outline; size: ButtonSize.sm; text: "Configure"; } } }
Item { index: 1; separator-before: true; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.MessageSquare; } ItemContent { ItemTitle { text: "Chat"; } ItemDescription { text: "Deploy notices in the releases channel."; } } ItemActions { Button { variant: ButtonVariant.outline; size: ButtonSize.sm; text: "Configure"; } } }
Item { index: 2; separator-before: true; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Palette; } ItemContent { ItemTitle { text: "Design"; } ItemDescription { text: "Design files linked to tickets."; } } ItemActions { Button { variant: ButtonVariant.outline; size: ButtonSize.sm; text: "Connect"; } } } } }}Usage
import { Item, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription, ItemActions } from "@glint/components/item.slint";import { Button, ButtonVariant, ButtonSize } from "@glint/components/button.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component AppWindow inherits Window { width: 520px; height: 160px; background: Tokens.color-background;
VerticalLayout { padding: 24px; alignment: start;
Item { ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.FileText; } ItemContent { ItemTitle { text: "Q3 report.pdf"; } ItemDescription { text: "1.4 MB · updated yesterday"; } } ItemActions { Button { variant: ButtonVariant.ghost; size: ButtonSize.sm; text: "Download"; } } } }}The row’s one slot is its band: media, content, actions, laid out left to
right. A component gets exactly one @children, written once and never inside
an if (ADR-0027), and the band is what Item spends it on — so a plain row
costs no wrapper at all.
That is also why header and footer are strings the row draws above and
below the band rather than slots of their own. ItemHeader and ItemFooter
stay exported as the type styles those lines use, for a stack built by hand.
| Part | What it is |
|---|---|
ItemMedia | The leading square: a slot, a lucide icon on a tile, or an image |
ItemContent | The middle cell. It takes the slack, so a long description never pushes the actions off the row |
ItemTitle / ItemDescription | The row’s name and the line under it. The title elides rather than widening its container |
ItemActions | The trailing cluster, centred against the band |
ItemGroup | The stack a set of rows sits in, and the list assistive technology walks |
Item is Card one density down, and it is named to read
that way: ItemTitle and ItemDescription stand to ItemContent exactly as
CardTitle and CardDescription stand to CardHeader. What it adds is the
row’s own axis and the opt-in whole-row trigger a list needs.
Examples
Variants
variant is the row’s surface treatment. ItemVariant.default draws nothing
at all — a row is a band of a list, and a list of bordered rows is a list of
cards. outline gives it a hairline edge, muted a fill.
import { Item, ItemVariant, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription } from "@glint/components/item.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 620px; height: 320px; background: Tokens.color-background;
VerticalLayout { padding: 24px; spacing: 12px; alignment: start;
Item { variant: ItemVariant.default; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Circle; } ItemContent { ItemTitle { text: "default"; } ItemDescription { text: "No surface — the row of a plain list."; } } }
Item { variant: ItemVariant.outline; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Square; } ItemContent { ItemTitle { text: "outline"; } ItemDescription { text: "A hairline edge and a wider radius."; } } }
Item { variant: ItemVariant.muted; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Triangle; } ItemContent { ItemTitle { text: "muted"; } ItemDescription { text: "A filled band, for a row that is set apart."; } } } }}Density
size packs the row: the padding, the gap between the band’s cells and the
media square all follow it. The square’s side is given rather than read — a
slotted component cannot see its host — so a row that changed its density hands
the number on with size: row.media-size. Sidebar’s regions are told about
their collapse the same way.
import { Item, ItemSize, ItemVariant, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription } from "@glint/components/item.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 620px; height: 320px; background: Tokens.color-background;
VerticalLayout { padding: 24px; spacing: 12px; alignment: start;
big := Item { size: ItemSize.default; variant: ItemVariant.outline; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Folder; size: big.media-size; } ItemContent { ItemTitle { text: "default"; } ItemDescription { text: "A 40px square and roomy padding."; } } }
small := Item { size: ItemSize.sm; variant: ItemVariant.outline; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Folder; size: small.media-size; } ItemContent { ItemTitle { text: "sm"; } ItemDescription { text: "32px, for a list that has to fit."; } } }
tiny := Item { size: ItemSize.xs; variant: ItemVariant.outline; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Folder; size: tiny.media-size; } ItemContent { ItemTitle { text: "xs"; } ItemDescription { text: "24px — the densest row."; } } } }}The media gutter
ItemMedia.variant is what the square holds. default is the slot: an
Avatar goes straight in, since Glint already has one
and a media variant for it would be a second spelling of the same thing.
icon draws a lucide glyph on a backing tile, and image draws a picture from
source, clipped to the square’s radius.
import { Item, ItemVariant, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription } from "@glint/components/item.slint";import { Avatar } from "@glint/components/avatar.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 620px; height: 260px; background: Tokens.color-background;
VerticalLayout { padding: 24px; spacing: 12px; alignment: start;
Item { variant: ItemVariant.outline; ItemMedia { variant: ItemMediaVariant.default; Avatar { fallback: "AL"; alt: "Ada Lovelace"; size: 40px; } } ItemContent { ItemTitle { text: "Ada Lovelace"; } ItemDescription { text: "Slotted — the gutter holds an Avatar."; } } }
Item { variant: ItemVariant.outline; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Paperclip; } ItemContent { ItemTitle { text: "contract.pdf"; } ItemDescription { text: "A lucide icon on a backing tile."; } } } }}A row that is a trigger
A row is a surface, not an affordance, until it is asked to be one — Card’s
rule. interactive is the ask: it gives the row a pointer cursor, a press dip,
a tab stop and a clicked callback. disabled is the refusal — the row dims,
keeps its place and swallows every activation path; selected is the current
row of a list, which wears the highlight the pointer raises.
The controls in ItemActions sit above the row’s own trigger, so they keep
their presses and their tab stops. A keyboard user reaches the row first, then
its actions.
import { Item, ItemVariant, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription, ItemActions } from "@glint/components/item.slint";import { Button, ButtonVariant, ButtonSize } from "@glint/components/button.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 640px; height: 320px; background: Tokens.color-background;
property <string> last: "Nothing pressed yet.";
VerticalLayout { padding: 24px; spacing: 10px; alignment: start;
Item { variant: ItemVariant.outline; interactive: true; label: "Open the September invoice"; description: "Paid on 3 October"; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Receipt; } ItemContent { ItemTitle { text: "September invoice"; } ItemDescription { text: "Paid on 3 October"; } } ItemActions { Button { variant: ButtonVariant.ghost; size: ButtonSize.sm; text: "Download"; clicked => { root.last = "The action was pressed."; } } } clicked => { root.last = "The row was pressed."; } }
Item { variant: ItemVariant.outline; interactive: true; selected: true; label: "Open the October invoice"; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Receipt; } ItemContent { ItemTitle { text: "October invoice"; } ItemDescription { text: "The selected row."; } } clicked => { root.last = "The selected row was pressed."; } }
Item { variant: ItemVariant.outline; interactive: true; disabled: true; label: "November invoice, not yet issued"; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Receipt; } ItemContent { ItemTitle { text: "November invoice"; } ItemDescription { text: "Refuses every activation path."; } } clicked => { root.last = "This never fires."; } }
Text { text: root.last; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; } }}An interactive row is named at the call site. Slint gives a host no way to
read the text it was slotted, so the row cannot borrow the ItemTitle inside
it: label and description are what assistive technology announces, and the
slotted title goes on speaking for itself.
A list of rows
ItemGroup is the list a set of rows sits in. It counts what it holds from
item-count rather than from its own children — Slint lets a host neither
address nor count what was slotted into it, which is the same reason each row
is told its own index.
A row with an index is a list item carrying that position and its selected
state; an interactive row without one, standing alone, is announced as a button
instead. Membership of a list is not an interaction, so a plain row inside a
group still has a position — and a row that is neither interactive nor indexed
has no node of its own at all.
separator-before opens a row with the group hairline. It belongs to the row
below it rather than being an entry of its own, so every child of the group
stays a real, indexable row (ADR-0012).
import { Item, ItemGroup, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription } from "@glint/components/item.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 620px; height: 300px; background: Tokens.color-background;
in-out property <int> current: 1;
VerticalLayout { padding: 24px; alignment: start;
ItemGroup { label: "Recent files"; item-count: 3;
Item { index: 0; interactive: true; selected: root.current == 0; label: "Roadmap.md"; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.FileText; } ItemContent { ItemTitle { text: "Roadmap.md"; } ItemDescription { text: "Edited 2 hours ago"; } } clicked => { root.current = 0; } }
Item { index: 1; separator-before: true; interactive: true; selected: root.current == 1; label: "Budget.xlsx"; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Sheet; } ItemContent { ItemTitle { text: "Budget.xlsx"; } ItemDescription { text: "Edited yesterday"; } } clicked => { root.current = 1; } }
Item { index: 2; separator-before: true; interactive: true; selected: root.current == 2; label: "Launch deck"; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Presentation; } ItemContent { ItemTitle { text: "Launch deck"; } ItemDescription { text: "Edited last week"; } } clicked => { root.current = 2; } } } }}A line above and below the band
header and footer are the row’s first line and its last — a group name over
the band, a timestamp under it. Empty draws nothing and costs nothing.
import { Item, ItemVariant, ItemMedia, ItemMediaVariant, ItemContent, ItemTitle, ItemDescription, ItemActions } from "@glint/components/item.slint";import { Badge, BadgeVariant } from "@glint/components/badge.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 620px; height: 240px; background: Tokens.color-background;
VerticalLayout { padding: 24px; alignment: start;
Item { variant: ItemVariant.outline; header: "Deployment · production"; footer: "Started 4 minutes ago by ada"; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.Rocket; } ItemContent { ItemTitle { text: "web-frontend @ 78c1f9f"; } ItemDescription { text: "Build 214 · eu-central-1"; } } ItemActions { Badge { variant: BadgeVariant.secondary; text: "Running"; } } } }}A stack built by hand
The four type styles and the hairline are exported so a column that never
becomes an ItemGroup can still be built out of the family’s own parts.
ItemSeparator is Separator under the item family’s name, so it takes the
same orientation — a vertical rule between two rows standing side by side —
and the same optional caption.
import { Item, ItemVariant, ItemHeader, ItemFooter, ItemTitle, ItemDescription, ItemSeparator, ItemContent, ItemMedia, ItemMediaVariant } from "@glint/components/item.slint";import { SeparatorOrientation } from "@glint/components/separator.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 660px; height: 300px; background: Tokens.color-background;
VerticalLayout { padding: 24px; spacing: 12px; alignment: start;
// The type styles, stacked by hand rather than by a row. VerticalLayout { spacing: 2px; ItemHeader { text: "PINNED"; } ItemTitle { text: "Incident 41 — elevated 5xx on checkout"; } ItemDescription { text: "Mitigated. Post-mortem due Friday."; } ItemFooter { text: "Opened 2 days ago"; } }
ItemSeparator { text: "Everything else"; }
// Two rows side by side, parted by an upright hairline. HorizontalLayout { spacing: 12px;
Item { variant: ItemVariant.outline; horizontal-stretch: 1; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.CircleCheck; } ItemContent { ItemTitle { text: "Checkout"; } ItemDescription { text: "Healthy"; } } }
ItemSeparator { orientation: SeparatorOrientation.vertical; }
Item { variant: ItemVariant.outline; horizontal-stretch: 1; ItemMedia { variant: ItemMediaVariant.icon; icon: IconSet.TriangleAlert; } ItemContent { ItemTitle { text: "Search"; } ItemDescription { text: "Degraded"; } } } } }}API Reference
Properties
| Property | Type | Default | Description |
|---|---|---|---|
variant | in ItemVariant | ItemVariant.default | Surface treatment — none, a hairline edge, or a muted fill. |
size | in ItemSize | ItemSize.default | Density: the media square, the padding and the band's gap together. |
header | in string | no default | A line above the band; empty draws nothing. |
footer | in string | no default | A line below it; empty draws nothing. |
interactive | in bool | false | Makes the whole row activatable. Off by default: a surface is not an affordance until it is asked to be one. |
disabled | in bool | false | A row that refuses: it dims, swallows every activation, keeps its place and reports itself unavailable (CONTEXT.md "Refusal"). Spelled the way every other Glint control that is not a text entry spells it. |
selected | in bool | false | The current row of a list — it wears the same highlight the pointer raises, and says so when it is announced. |
separator-before | in bool | false | Opens the row with the group hairline. It belongs to the row below it rather than being an entry of its own, so every child of a group stays a real, indexable row (ADR-0012). |
index | in int | -1 | Where the row stands in its list. -1 — the default — is a row standing alone, which is announced as a button; anything else is a list item carrying this index. A slotted child cannot read its host, so the position is given rather than counted. |
label | in string | no default | What assistive technology announces the row as, and the line under it. An interactive row needs the name: the title it was slotted is out of reach. |
description | in string | no default | |
media-size | out length | no default | The media square's side at this density, published because the call site is what hands it to the ItemMedia it slotted. |
surface-color | out color | no default | The surface behind the row, and its edge — published so a slotted action can match the row it sits on. |
edge-color | out color | no default |
Callbacks
| Callback | Description |
|---|---|
clicked() | Fired when a live, interactive row is activated by pointer, by Enter or Space, or through the accessible default action. |
Enums
| Enum | Values |
|---|---|
ItemVariant | default, outline, muted |
ItemSize | default, sm, xs |
ItemGroup
The list a set of rows sits in.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
label | in string | no default | What the list is called. A page with two lists needs them told apart. |
item-count | in int | no default | How many rows it holds, for assistive technology's "3 of 12". |
spacing | in length | 0px | Gap between two rows. Rows touch by default, which is what makes separator-before read as one hairline between them. |
ItemMedia
The leading square.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
variant | in ItemMediaVariant | ItemMediaVariant.default | What the gutter holds. |
icon | in LucideIcon | no default | The lucide icon drawn by the icon variant. |
source | in image | no default | The picture drawn by the image variant. |
size | in length | 40px | The square's side. Item.media-size is the number that follows the row's density. |
Enums
| Enum | Values |
|---|---|
ItemMediaVariant | default, icon, image |
ItemContent
The band’s middle cell: the title, the description, and anything else the row stacks under them.
ItemContent publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint VerticalLayout it inherits.
ItemTitle
The row’s name. It elides rather than growing its container, and takes a zero minimum width so a narrow list does not overflow.
ItemTitle publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint Text it inherits.
ItemDescription
The line under the title, at the muted tone and wrapping by word.
ItemDescription publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint Text it inherits.
ItemActions
The trailing cluster. Centred against the band rather than stretched down it, so a two-line row does not stand its buttons at the top.
ItemActions publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint VerticalLayout it inherits.
ItemHeader
The row’s first line, as a type style — for a stack built by hand.
ItemHeader publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint Text it inherits.
ItemFooter
The row’s last line, as a type style.
ItemFooter publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint Text it inherits.
ItemSeparator
The hairline between two rows, under the item family’s own name. It is
Separator, so it carries that component’s
orientation and caption.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
orientation | in SeparatorOrientation | SeparatorOrientation.horizontal | horizontal (default) or vertical. Ignored while there is a caption, which is horizontal by construction. |
text | in string | no default | Caption centred in the rule; empty (the default) is the bare hairline. |
Enums
| Enum | Values |
|---|---|
SeparatorOrientation | horizontal, vertical |
Accessibility
- A plain row has no node. An
Itemthat is neither interactive nor indexed declares no role: what a screen reader reads is what you put inside it. That is Card’s rule at one lower density. - Two nodes, one of which exists.
accessible-rolemust be a constant in Slint, so a row standing alone (index: -1, the default) is announced as abuttonand a row inside a list as alist-itemcarrying its index and its selected state. Only the button half is gated oninteractive— membership of a list is not an interaction. - The name is the call site’s. A host cannot read the text it was slotted,
so
labelanddescriptionare what the row announces. The slottedItemTitlekeeps speaking for itself; there is nothing here that could silence it. - One activation path. The pointer, Enter and Space, and the accessible
default action all land in one place, and that is where
disabledrefuses. The refusal is never theTouchArea’senabled— a disabled TouchArea is transparent to the pointer, and the press would land on whatever sits under the row. - A row nobody can activate takes no tab stop. An affordance that does nothing is not one. An interactive row takes the stop before the controls it holds, so a keyboard user reaches the row and then its actions.
ItemGroupis the list. It carriesaccessible-role: listwith itslabelanditem-count, which is what gives assistive technology its “3 of 12”. The count is a property because a host can neither address nor count what was slotted into it.- The media gutter is decoration. An
ItemMediaholding an icon adds no node of its own; a slottedAvatarannounces itself through its ownalt.