Stacked list

Repeating rows with optional leading and trailing regions

Intent

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.

Basic usage

.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.

  • Design assets

    12 items · Updated 2 hours ago

  • Q1 Report

    2.4 MB · Updated yesterday

  • Brand guidelines

    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>

Dividers

.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.

  • Design assets

    12 items · Updated 2 hours ago

  • Q1 Report

    2.4 MB · Updated yesterday

  • Brand guidelines

    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>

Compact

.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>

Leading region

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.

  • Design assets

    12 items · Updated 2 hours ago

  • Q1 Report.xlsx

    2.4 MB · Updated yesterday

  • Brand Guidelines.pdf

    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>

Trailing region

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.

  • Design assets

    12 items · Updated 2 hours ago

    Shared
  • Q1 Report.xlsx

    2.4 MB · Updated yesterday

    Synced
  • Brand Guidelines.pdf

    8.1 MB · Updated last week

    Review
<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>

Interactive rows

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.

<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>

Revealable rows

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.

  • Q1 Report.xlsx

    2.4 MB · Updated yesterday

    Synced

    Final quarterly numbers for the leadership review. Regional breakdowns, gross margin trends, and the headcount summary.

    Open Share Download

  • Brand Guidelines.pdf

    8.1 MB · Updated last week

    Review

    Logo lockups, color tokens, and approved typeface pairings. Two pages flagged for the next round of review; comments are pending.

  • Archive (legacy)

    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>

Reference

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.

Customization

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.

Accessibility

  • The row's primary content is the heading inside the content <div>. Use a heading level appropriate to the surrounding outline.
  • The .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.
  • For interactive rows, wrap the heading text in <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.
  • For revealable rows, the native <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.
  • If a row's leading icon is decorative, mark its <svg> with aria-hidden="true". If the icon conveys meaning the heading does not (e.g., status), give the <svg> a <title> child.
  • If multiple actions live in the trailing region, the dropdown trigger needs an aria-label identifying the row (e.g., "Actions for Q1 Report") so it is distinguishable when listed across rows.

CSS Reference: Stacked List

Vertical row stack for repeating-row UIs

Source: src/css/stacked-list.css

Tokens

TokenDefaultDescription
--stacked-list-padding-blockvar(--ui-pad-y-snug)Row vertical padding
--stacked-list-padding-inlinevar(--ui-pad-x-snug)Row horizontal padding
--stacked-list-gapvar(--gap-snug)Gap between leading, content, and trailing regions
--stacked-list-radiusvar(--radius-sm)Row corner radius, visible on hover
--stacked-list-content-gapvar(--space-3xs)Gap between heading and description in the content region
--stacked-list-divider-colorvar(--color-border-subtle)Border color between rows when .is-divided is active
--stacked-list-leading-alignflex-startVertical alignment of the leading slot
--stacked-list-trailing-alignflex-startVertical alignment of the trailing slot
--stacked-list-header-font-sizevar(--step-0)Heading font size
--stacked-list-content-font-sizevar(--step--1)Description and paragraph font size
--stacked-list-chevron-size0.5emSize of the revealable-row chevron
--stacked-list-chevron-colorvar(--color-text-muted)Color of the revealable-row chevron

Slots

SlotDescription
> :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

Classes

ClassDescription
.stacked-listcomponent root
.is-dividedAdds 1px border between sibling rows
.is-compactDenser padding and tighter gap
.is-interactiveHover highlight and stretched heading link for whole-row click