Design assets
12 items · Updated 2 hours ago
Repeating rows with optional leading and trailing regions
Stacked list is for repeating rows where each row carries chrome (padding, optional divider, optional hover) and an (optional leading) | content | (optional trailing) layout. Common shapes: file and asset browsers, ticket queues, notification feeds, settings rows.
For single-line nav rows with current-page state and a rail, use sidebar nav. For menu rows inside a popover, use dropdown. Stacked list sits between -- two-line content rows that aren't navigation and aren't a menu.
.stacked-list goes on a
<ul>,
<ol>, or <div>. Each row is
an <article>
for self-contained pieces or a <div>
otherwise. Wrap the row in <li> when the
container is a list. <section> rows are
accepted but add nothing while unnamed, and a named one becomes
a region landmark per row; prefer <article>
or <div>.
The row's primary content lives in a bare
<div>; that div is itself a flex column, so a
heading and a paragraph stack naturally.
12 items · Updated 2 hours ago
2.4 MB · Updated yesterday
8.1 MB · Updated last week
<ul class="stacked-list" style="max-inline-size: 32rem">
<li>
<article>
<div>
<h3>Design assets</h3>
<p>12 items · Updated 2 hours ago</p>
</div>
</article>
</li>
<li>
<article>
<div>
<h3>Q1 Report</h3>
<p>2.4 MB · Updated yesterday</p>
</div>
</article>
</li>
<li>
<article>
<div>
<h3>Brand guidelines</h3>
<p>8.1 MB · Updated last week</p>
</div>
</article>
</li>
</ul>
.is-divided draws a 1px border between sibling rows
using the
--color-border-subtle token by default.
The divider sits at row 2..N's block-start, so the first row has
no leading divider.
12 items · Updated 2 hours ago
2.4 MB · Updated yesterday
8.1 MB · Updated last week
<ul class="stacked-list is-divided" style="max-inline-size: 32rem">
<li>
<article>
<div>
<h3>Design assets</h3>
<p>12 items · Updated 2 hours ago</p>
</div>
</article>
</li>
<li>
<article>
<div>
<h3>Q1 Report</h3>
<p>2.4 MB · Updated yesterday</p>
</div>
</article>
</li>
<li>
<article>
<div>
<h3>Brand guidelines</h3>
<p>8.1 MB · Updated last week</p>
</div>
</article>
</li>
</ul>
.is-compact remaps row padding and gap to the
compact density tier. Useful for sidebar lists, condensed
archives, and dense meta-views where vertical rhythm matters
more than per-row breathing room.
<ul class="stacked-list is-compact is-divided" style="max-inline-size: 18rem">
<li>
<article class="is-interactive">
<div>
<h4><a href="#">April</a></h4>
</div>
<div class="trailing">3</div>
</article>
</li>
<li>
<article class="is-interactive">
<div>
<h4><a href="#">March</a></h4>
</div>
<div class="trailing">4</div>
</article>
</li>
<li>
<article class="is-interactive">
<div>
<h4><a href="#">February</a></h4>
</div>
<div class="trailing">2</div>
</article>
</li>
</ul>
The leading region is a <div class="leading">
placed before the content div -- a fit for a file-type icon, an
avatar, or a status glyph. The class is the hook, so any element
works; <div> is the canonical choice because
a generic element carries no ARIA role and so adds no landmark
noise, wherever the list sits in the page.
Legacy markup: an <aside> before the content
div (leading) or after it (trailing) still works, but isn't
suggested. An unnamed <aside> stays out of
the landmark tree only when an <article> or
other sectioning element sits above it; drop the same list
directly into <main> with
<div> rows and every slot becomes a
complementary landmark. The classed slots behave the same
everywhere, and accessibility checkers stop flagging the rows.
Leading icons inherit --color-text-muted by default.
Apply utility classes (.text-primary,
.text-success, etc.) on the slot to color-code by
category.
12 items · Updated 2 hours ago
2.4 MB · Updated yesterday
8.1 MB · Updated last week
<ul class="stacked-list is-divided" style="max-inline-size: 32rem">
<li>
<article>
<div class="leading text-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<div>
<h3>Design assets</h3>
<p>12 items · Updated 2 hours ago</p>
</div>
</article>
</li>
<li>
<article>
<div class="leading text-success">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div>
<h3>Q1 Report.xlsx</h3>
<p>2.4 MB · Updated yesterday</p>
</div>
</article>
</li>
<li>
<article>
<div class="leading text-danger">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div>
<h3>Brand Guidelines.pdf</h3>
<p>8.1 MB · Updated last week</p>
</div>
</article>
</li>
</ul>
The trailing region is a <div class="trailing">
placed after the content div. It can hold a single value (a
count, a timestamp), a badge, or a
dropdown trigger -- and a
cluster when more than one of
these need to sit together. The flex layout pushes the trailing
slot to the row's end automatically; no positioning hack
required.
12 items · Updated 2 hours ago
2.4 MB · Updated yesterday
8.1 MB · Updated last week
<ul class="stacked-list is-divided" style="max-inline-size: 36rem">
<li>
<article>
<div class="leading text-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<div>
<h3>Design assets</h3>
<p>12 items · Updated 2 hours ago</p>
</div>
<div class="trailing">
<span class="cluster gap-2xs">
<span class="badge is-accent text--2">Shared</span>
</span>
</div>
</article>
</li>
<li>
<article>
<div class="leading text-success">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div>
<h3>Q1 Report.xlsx</h3>
<p>2.4 MB · Updated yesterday</p>
</div>
<div class="trailing">
<span class="badge is-success text--2">Synced</span>
</div>
</article>
</li>
<li>
<article>
<div class="leading text-danger">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div>
<h3>Brand Guidelines.pdf</h3>
<p>8.1 MB · Updated last week</p>
</div>
<div class="trailing">
<span class="badge is-warning text--2">Review</span>
</div>
</article>
</li>
</ul>
Add .is-interactive to the row to make the whole
row clickable. The pattern mirrors
.card.is-interactive:
the heading's anchor receives a stretched ::after
that fills the row, so a click anywhere in the row activates the
link. The link's accessible name stays the heading text -- a
cleaner outcome than wrapping the entire row in an
<a> (which would name the link with the
entire row's text and forbid nested interactives).
Nested interactive elements in the trailing slot (a dropdown
trigger, a button) sit above the stretched layer automatically.
Other nested interactives the consumer adds inside the content
<div> are the consumer's responsibility to
lift with position: relative.
Taylor Kim · #312 · 3 hours ago
Casey Park · #308 · yesterday
Jordan Lee · #305 · 2 days ago
<ul class="stacked-list is-divided" style="max-inline-size: 36rem">
<li>
<article class="is-interactive">
<div class="leading">
<span class="avatar is-sm">
<img src="https://picsum.photos/seed/sl1/64/64" alt=""/>
</span>
</div>
<div>
<h3><a href="#">Build failing after dependency update</a></h3>
<p>Taylor Kim · #312 · 3 hours ago</p>
</div>
<div class="trailing">
<span class="badge is-danger text--2">Open</span>
</div>
</article>
</li>
<li>
<article class="is-interactive">
<div class="leading">
<span class="avatar is-sm">
<img src="https://picsum.photos/seed/sl2/64/64" alt=""/>
</span>
</div>
<div>
<h3><a href="#">Pagination offset off by one</a></h3>
<p>Casey Park · #308 · yesterday</p>
</div>
<div class="trailing">
<span class="badge is-success text--2">Resolved</span>
</div>
</article>
</li>
<li>
<article class="is-interactive">
<div class="leading">
<span class="avatar is-sm">
<img src="https://picsum.photos/seed/sl3/64/64" alt=""/>
</span>
</div>
<div>
<h3><a href="#">Color tokens missing in dark mode</a></h3>
<p>Jordan Lee · #305 · 2 days ago</p>
</div>
<div class="trailing">
<span class="badge is-warning text--2">In review</span>
</div>
</article>
</li>
</ul>
For rows that need to disclose extra content in place, use a
<details>
as the row element. The three-region layout lives on
<summary>; everything else inside the
<details> is the reveal pane. A chevron at
the trailing edge rotates on open, matching the affordance from
details.
The reveal pane inherits the row's inline padding so its content aligns with the summary; otherwise it's an unopinionated slot. Drop in plain prose, a cluster of actions, or any composition that fits.
Revealable rows and .is-interactive rows are
different shapes -- a row is one or the other. The stretched
heading-anchor click target and the native
<summary> click target would compete; pick
the shape that suits the row.
8.1 MB · Updated last week
Logo lockups, color tokens, and approved typeface pairings. Two pages flagged for the next round of review; comments are pending.
Read-only · Last touched 2024
<ul class="stacked-list is-divided" style="max-inline-size: 36rem">
<li>
<details>
<summary>
<div class="leading text-success">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div>
<h3>Q1 Report.xlsx</h3>
<p>2.4 MB · Updated yesterday</p>
</div>
<div class="trailing">
<span class="badge is-success text--2">Synced</span>
</div>
</summary>
<div class="flow">
<p>
Final quarterly numbers for the leadership review.
Regional breakdowns, gross margin trends, and the
headcount summary.
</p>
<p class="cluster gap-2xs">
<a href="#" class="button is-sm">Open</a>
<a href="#" class="button is-sm is-ghost">Share</a>
<a href="#" class="button is-sm is-ghost">Download</a>
</p>
</div>
</details>
</li>
<li>
<details>
<summary>
<div class="leading text-danger">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div>
<h3>Brand Guidelines.pdf</h3>
<p>8.1 MB · Updated last week</p>
</div>
<div class="trailing">
<span class="badge is-warning text--2">Review</span>
</div>
</summary>
<div>
<p>
Logo lockups, color tokens, and approved typeface
pairings. Two pages flagged for the next round of
review; comments are pending.
</p>
</div>
</details>
</li>
<li>
<article>
<div class="leading text-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<div>
<h3>Archive (legacy)</h3>
<p>Read-only · Last touched 2024</p>
</div>
</article>
</li>
</ul>| Name | Role |
|---|---|
.stacked-list |
Container. Flex column, list-style none. Goes on <ul>, <ol>, or <div>. |
<article> / <div> / <details> (row) |
Row element. <article> for self-contained pieces, <div> otherwise, <details> for revealable rows. <section> is accepted but not suggested; a named one becomes a region landmark per row. |
<summary> (revealable rows) |
Inside a <details> row, <summary> hosts the three-region layout and the disclosure chevron. The reveal pane is every other child of the <details>; it inherits the row's inline padding. |
.leading |
Leading metadata region, placed before the content <div>. Common for icons or avatars. Goes on a <div>. |
<div> (content) |
Content region between the slots: any direct-child <div> without a slot class. Holds the row's heading and description. |
.trailing |
Trailing metadata region, placed after the content <div>. Common for actions, status, timestamps. Goes on a <div>. |
<aside> (legacy slots) |
An aside before the content <div> matches the leading slot; one after it matches the trailing slot. Still supported, not suggested -- landmark exposure depends on the markup around the list. |
.is-divided |
1px border-block-start between sibling rows. Color via --stacked-list-divider-color. |
.is-compact |
Density preset. Remaps padding and gap to the compact tier. |
.is-interactive |
Per-row, on the row element. Stretches the heading anchor's click area over the whole row; adds hover bg and a focus ring. |
Tokens scope to .stacked-list and cover row
padding, the gap between regions, corner radius (visible on
hover), the gap between heading and description, and the
divider color. Two further tokens --
--stacked-list-leading-align and
--stacked-list-trailing-align -- govern how the
leading and trailing slots sit
vertically; both default to flex-start so icons
and avatars top-align with the heading. Override per-region
when a centered or end-aligned trailing reads better for the
row's shape.
Heading and description typography are also tokenized
(--stacked-list-header-font-size,
--stacked-list-content-font-size) so retuning the
visual size doesn't depend on which heading level (h2-h6) the
author chose. Pick the level that fits your document hierarchy
and override the tokens when the row's content needs a different
weight. Full token list and defaults:
src/css/stacked-list.css.
<div>. Use a heading level appropriate to
the surrounding outline..leading and .trailing slots
are generic <div>s: no ARIA role, no
landmark exposure, no per-row noise for assistive
technology. The legacy <aside> slots
don't share that guarantee -- whether an unnamed aside is
exposed as generic or as a complementary landmark depends
on the nearest sectioning ancestor, so prefer the classed
slots.<a> and add .is-interactive
to the row. The link's accessible name is just the heading
text -- cleaner than a whole-row anchor that names the link
with the entire row content. The row receives a focus ring
when the heading anchor is focused via
:focus-visible; do not suppress it.<summary> is the click target and is
keyboard-operable (Enter / Space toggles); the browser draws
its own focus ring. Do not nest interactive elements
(links, buttons) inside <summary> -- they
cause click-target ambiguity. Put nested actions in the
reveal pane instead.<svg> with aria-hidden="true".
If the icon conveys meaning the heading does not (e.g.,
status), give the <svg> a
<title> child.aria-label
identifying the row (e.g., "Actions for Q1 Report") so it
is distinguishable when listed across rows.Vertical row stack for repeating-row UIs
Source: src/css/stacked-list.css
| Token | Default | Description |
|---|---|---|
--stacked-list-padding-block | var(--ui-pad-y-snug) | Row vertical padding |
--stacked-list-padding-inline | var(--ui-pad-x-snug) | Row horizontal padding |
--stacked-list-gap | var(--gap-snug) | Gap between leading, content, and trailing regions |
--stacked-list-radius | var(--radius-sm) | Row corner radius, visible on hover |
--stacked-list-content-gap | var(--space-3xs) | Gap between heading and description in the content region |
--stacked-list-divider-color | var(--color-border-subtle) | Border color between rows when .is-divided is active |
--stacked-list-leading-align | flex-start | Vertical alignment of the leading slot |
--stacked-list-trailing-align | flex-start | Vertical alignment of the trailing slot |
--stacked-list-header-font-size | var(--step-0) | Heading font size |
--stacked-list-content-font-size | var(--step--1) | Description and paragraph font size |
--stacked-list-chevron-size | 0.5em | Size of the revealable-row chevron |
--stacked-list-chevron-color | var(--color-text-muted) | Color of the revealable-row chevron |
| Slot | Description |
|---|---|
> :is(.leading, aside:has(~ div)) | Leading slot -- .leading, or a legacy <aside> that has a content <div> after it. Aligns to flex-start by default; tokenized so consumers can center an avatar in tightly-packed rows, etc. |
> :is(.trailing, div ~ aside) | Trailing slot -- .trailing, or a legacy <aside> after a content <div>. Same flex-start default, independent token. |
> div:not(.leading, .trailing) | Content region -- bare <div> between the slots. Itself a flex column so heading and description stack with a small gap without consumers needing to wrap them in .flow. |
:is(h2, h3, h4, h5, h6) | Header region |
| Class | Description |
|---|---|
.stacked-list | component root |
.is-divided | Adds 1px border between sibling rows |
.is-compact | Denser padding and tighter gap |
.is-interactive | Hover highlight and stretched heading link for whole-row click |