Item list
The canonical listing (AD ruling 20 Jul 2026, docs/PLAN-listview.md): one row pattern for tasks, e-mails, activities, notes, files and queues. Specialisations differ by what goes into the slots — never by the frame. React apps/web/src/components/ItemList.tsx, CSS apps/web/src/ds/item-list.css (.il-*).
Record list (.rl-*) is the rejected predecessor of this pattern: do not migrate onto it, do not extend it.
When to use
- Any list of items the user scans and opens: tasks, e-mails, activities, notes, files, queues, search results.
- Inside a page, a panel, a drawer or a record tab — the width mode adapts, the row does not.
When not to use
- A data table with columns the user compares and sorts — that is Table.
- The fields of one record — that is Record rail.
- Navigation between screens — that is the sidebar.
- A grid of cards.
Anatomy
- Container
ul.il.il-<density>.il-<width>withrole="list"; each row sits inli.il-li[role="listitem"]. - Row
.il-row: a<button>when it opens something, a<div>when it does not. - Slots:
.il-lead(20px — icon, avatar or checkbox; exactly one thing) ·.il-t(title) ·.il-by(byline) ·.il-c(content) ·.il-tags(label pills, optional) ·.il-meta(date, assignee). There is no trailing slot — fixed columns are a separate variant (when+cols, below). - Densities:
sm28px (compact, one line) ·md44px (one line) ·lg52px (two lines, the content moves to the second line). lg+previewLines={2}(.il-lg.il-c2): the content preview wraps to two lines (72px row) — only when the preview is the point, e.g. mail in Ajda's reply. Opt-in on the container; every otherlglist keeps one preview line.- A row with nothing but a title (the quiet "and N more" / "Open in mail (N)" row) keeps the
mdheight in every density —lgreserves no empty second line for it. - Tags: the
tagsslot sits between the content and.il-meta— see "Tags (tags) vs columns (cols)" below. - Width:
page(centred, capped by--il-page-max) ·embedded(edge-to-edge; rows give back the host's gutter, overridable with--il-gutter). - Dividers: a hairline under every row, none under the last. Variant
.il-seamlessdrops them entirely — see below.
Variant: no dividers (.il-seamless)
Two kinds of list, not one list with an exception. With dividers is a listing of DATA: long, scanned across the width, several slots per row (mail, tasks, record lists) — the hairline guides the eye and the canon of listings (AD 20 July 2026) prescribes it.
Without dividers is a SHORT SELECTION inside a section: the command palette, a picker, items under a section label. Rhythm is carried by space and alignment; a hairline between short rows reads as a table and turns a choice into a grid.
Turn it on from the outside — <ItemList className="il-seamless"> — so the consumer asks for it, not the row. .il-row itself is unchanged.
Product owner, 2 September 2026 (D40): „I don’t want the individual rows separated by lines. Just the header where the search query is typed, and then the listing without separating the rows.“ AD ruling 3 September 2026 (D55): this is a second kind of list, not a one-screen exception. Two places had already switched the dividers off ad hoc for the same reason, which is what made it a canon need rather than one screen’s wish.
Variant: columns (when + cols)
For a listing where every row is a record with several facts of the same kind under each other — the Setkání list (meetings and calls): time · type · title · company · people · result · processing · owner. AD approved the prototype 27 Sep 2026 (AKT-3, acrm !3970).
whenrenders.il-whenbefore the lead: the time is the anchor of a day-grouped list and is read first; inmetaon the right it got lost. A secondary part (duration) goes into.acrm-noteinside it.colsrenders fixed-width.il-colcells after the title. Each column carries its own width (--il-col-w), so columns line up across rows and across day groups; the title takes what is left.end: truealigns the content to the end (owner avatar).- A long name inside a column truncates with an ellipsis (
.eref-chip-nm) and the overflow count+N(.tl-more, the quiet mono of Deal card) always stays visible; the full list goes into itstitle. Same canon as the Provázané slot of the task list — a hard clip of the column looks like a defect. - Status inside a column is
.il-dot+ a word. The dot is the.acrm-pill::beforedot (8 px,--ic-*) without the pill frame — pill frames stacked in a column are noise. Tones:green/amber/ grey for state,criticalonly for an error,blue/rose/purplefor deal stages. - One axis per column. Do not mix two kinds of fact in one column (deal result and minutes state are two columns — AD 27 Sep 2026).
- An action inside a column is
<Button compact>or<SplitButton compact>(24 px, the only sizes meant for rows); the row then renders asdiv[role=button](controls), never a button inside a button. - Desktop only. On a phone the same record is an
lgrow: title on the first line,time · duration · company · resulton the second, the status inmeta.
Tags (tags) vs columns (cols)
Owner 8 Oct 2026, acrm !4915 (merged 10 Oct 2026), AD review FA258 / FA259 / FA264.
A label of the row goes into tags. Always. A label is a pill that names a category of the item — an e-mail label, a task tag, the project a message belongs to (Project label). It is not a fact in a column.
tagsrenders.il-tagsright-aligned, directly before.il-meta(the date). No fixed width: the old solution, a 200 pxcolscolumn, left a gap between pills and date that depended on how many tags a row had.- The group may shrink (the pills clip from the left, every child has
min-width: 0) but never grows past half of the row — the title wins. - Pills are
.acrm-pill sm. A pill with a leading icon (Project label) carries the tone in the icon, see Pill. --il-meta-w: the list sets it on its wrapper to give every.il-metathe same width (the widest date format of that list). The right edge of the tags then lines up across rows and day groups. Without the variable.il-metakeeps its natural width and nothing changes — set it only for a list with tags, and the same value for every row of that list.colsstays for facts of one kind under each other (time, company, people, owner, status dot + word). A status.acrm-pillinside a table column is a state, not a tag, and stays where it is. Do not write a column for tags and do not put pills inmeta.- Max two or three pills per row; the rest folds into
+N(.tl-more) with the full list intitle.
Phone (density lg)
On lg the tags would squeeze the title to a few letters next to the date, so the component itself puts them on their own line below the title and the date, full width (.il-tags-below, the row gets .il-tags-wrap), left-aligned and wrapping. With a lead the line is indented to the title (.il-tags-wrap-lead). The consumer passes the same tags on every density; it never branches on the screen width. lg rows have padding-block: var(--sp-3) (the two-line row needed more air above and below).
Group bar over an embedded list (.tl-grp.is-embedded)
A group header (.tl-grp: day, person, channel, project) above a list with width="embedded" spans the full width of the box, exactly like the list’s hairlines — the modifier .is-embedded on the header takes the host gutter back (--il-gutter). Set it explicitly; do not rely on :has and do not patch the header per screen (Hovory and the timeline used to).
Variant: listbox cursor (role="listbox")
For the ⌘K palette's keyboard cursor (ACRM-322, owner 26 Sep 2026, delivered in acrm !3997). Applies only when the container's role prop is "listbox" — see the Accessibility doc for why that role is an opt-in, not the canon.
- The cursor no longer paints
.il-cursoras a per-row background. One persistent element,.il-cursor-bar, sits behind the rows and slides to the row under the cursor on the same spring as a dialog window (springAnimate,packages/ui/src/lib/spring.ts). Moving the cursor a short distance (arrow keys inside the same list) rides the spring; the cursor jumping in from elsewhere (a different section, new results replacing the list) makes the bar snap into place and only fade in — it never flies in over content that was not there a moment ago. - The whole listbox cross-fades in on mount or on a new set of results (
animation: ilListIn 0.2s ease-out) so it never blinks as the user keeps typing. - The kbd hint on the cursor row (→, ↵ in
.il-meta .acrm-kbd) belongs to the destination of the move: it stays hidden while the bar is travelling ([data-cursor-moving]on the container, cleared afterSPRING_ARRIVE_MS) and only fades in once the bar has landed — it must not appear to arrive ahead of the highlight. prefers-reduced-motion: reduceturns off both the bar's spring andilListIn— the cursor and the list appear instantly, no travel, no fade.- Today's only consumer is
GlobalSearch.tsx(the ⌘K palette). Do not reach for this variant to paint a selected row in an ordinaryrole="list"— that isil-active(see Best practices), a different state with a different meaning.
Row actions (acrm !4531, 3 Oct 2026)
Row actions go in the meta slot wrapped in .il-acts (one or more icon buttons): hidden until the row is hovered or has focus inside, space reserved so the row never jumps, always visible on touch and narrow screens with a --tap-min target — the same contract as the rail's .rr-acts. Hidden with opacity, never visibility, so the buttons stay in the Tab order and keyboard focus reveals them. Set controls on the row so it is not a button around buttons. First consumer: allowed domains in Settings › Access.
Reordering (acrm !4488, 3 Oct 2026)
A list whose order the user owns (milestones of a project, custom task order) is reordered in place. There is one mechanism for the whole product — useReorder in @acrm/ui — and ItemList switches it on with onReorder(nextKeys, movedKey); every row that can move carries reorderKey. Pass onReorder only when the user may change the order; without it the list behaves exactly as before.
- Mouse: press and move a few pixels to lift the row; release over the target. A grip (
grip-vertical,--ico-sm) sits in the row's left padding and appears on hover or focus, like the rail's pencil; it never shifts the content. - Touch: hold the row for about a third of a second to lift it, then drag. Moving the finger before that is a scroll, so the list still scrolls normally. A short tap still opens the row. There is no grip on touch.
- Keyboard: Alt+↑ / Alt+↓ moves the focused row by one place. Focus stays on the moved row and a screen reader hears the new position („Krypto přesunuto na 3. místo z 6“).
- Look: a floating copy of the row (or card) follows the pointer —
liftDragGhostin@acrm/ui(packages/ui/src/lib/dragGhost.ts), class.acrm-drag-ghost(--bg-canvas,--shadow-pop, z 75); the original is dimmed to 0.45 (.is-lifted) and closes up behind it. Items in the target move apart (dropGapin@acrm/ui, inlinetransformon[data-gap-shift], 160 ms only while dragging) and the gap shows the landing slot.acrm-drop-slot— 2px dashed--accent,--accent-tintfill,--radius-md, the height of the dragged item, z 74 under the copy (ds/reorder.css). The insertion index is computed from the natural positions without the dragged item, so the gap under the pointer does not flicker. One look for every drag in the product, rows and board cards alike (owner 6 Oct 2026, acrm !4774, !4776, !4796); the 3px landing line.acrm-reorder-lineis gone. Consumers switch this look on; they never draw their own ghost or slot. - A click that arrives right after a drag is swallowed, so dropping a row never opens it.
- Blocks: child rows (
data-reorder-child, subtasks) travel with their parent; rows after the last movable item (done tasks, the add row, „Bez milníku“) stay under the gap. While dragging, text selection is off (html.acrm-reordering) and only the lifted row shows its grip. - Board (kanban, Obchody and Úkoly): the same gesture across columns — the column is a group, the target column and insertion index come from the pointer position. Near the board edge the columns scroll by themselves, near a column edge the cards do (speed grows with depth in a 56px zone, no jump), so a card can be dragged to a stage off screen on a phone. Keyboard: Alt+← / Alt+→ moves the card to the neighbouring stage (to the end), Alt+↑ / Alt+↓ within a column; the screen reader hears stage and position („… přesunuto do Canvas, 8. z 8“). „Prohráno“ is never a keyboard shortcut — only the reason dialog. Without manual sorting (by date or value) the gap opens where the page sort will place the card (
landingIndex); a column that does not accept the card (dropDisabled) does not open. The target column highlight and the lost-flag stay board-specific; while dragging the phone FAB steps aside so the flag is fully reachable. - The same hook drives
ItemList,TaskList(acrm !4492),CheckMenu(acrm !4493), the milestone blocks of a project (acrm !4495) and the kanban board (acrm !4509): one look, one keyboard and one touch behaviour in the whole product. Never add a fourth drag implementation — switch the list ontouseReorder. - Rows that are not part of the order (an add row, „Bez milníku“) simply have no
reorderKey; dropping below the last movable row puts the item at the end.
Densities on touch
sm rows are 28px for dense desktop lists (palette, rail sections). On devices without hover every sm row grows to --tap-min (44px), so it is a comfortable touch target; desktop stays at 28px.
Best practices
Do
- Keep one typography for everything, including the date (500 13/20). Hierarchy is carried by colour alone — never bold, never a second size, never mono.
- Put exactly one thing in the lead slot.
- Choose the density by content, not by taste:
smwhere the list is a tool (a palette),mdfor ordinary listings,lgonly when a preview line genuinely helps. - Let the host own the gutter:
embeddedrows pull it back themselves; do not add negative margins around the list. - Load more rows by 50 as the user scrolls; no sticky bar at the bottom.
Don't
- Add a trailing slot or a row-actions column; if an item needs actions, they belong to the row's own screen or a menu.
- Bold a title to mark "unread" — that is what the
readtone is for. - Mix densities in one list.
- Draw label pills in
colsormeta, or give them a column of their own — labels go totags. - Build a new row pattern for a new kind of item; the slots are the extension point.
Content
- Title is the item as the user would name it; no type prefix.
- Byline is who or what it belongs to; meta is when, in the year convention (
26. 8.,14. 11. 27). - The empty state speaks plainly (
Zatím tu nic není), inside the frame.
Related
record-list— the rejected predecessor, kept only because screens still render it.task-list,email-list,timeline— specialisations that fill these slots.table— when the user compares columns instead of scanning rows.- Stories:
components--item-list--default,--variants,--sizes,--states,--with-icon-and-kbd,--composition.
Source: docs/item-list/item-list.usage.md · GitLab