List group

A stack of rows on a shared rounded surface.

Basic

Add .list-group to the wrapper and .list-group__item to each row. Use any list element you like, such as <ul>, <ol>, or a plain <div>.

  • Frontend platform
  • Mobile apps
  • Data infrastructure
  • Customer success
  • Legal & compliance
<ul class="list-group w-100" style="max-width: 24rem;">
  <li class="list-group__item">Frontend platform</li>
  <li class="list-group__item">Mobile apps</li>
  <li class="list-group__item">Data infrastructure</li>
  <li class="list-group__item">Customer success</li>
  <li class="list-group__item">Legal &amp; compliance</li>
</ul>

Active and disabled

Mark the selected row with data-state="active" (or aria-current on a link). Mark unreachable rows with aria-disabled="true". Active takes the highlight surface; disabled fades and blocks pointer events.

  • Profile
  • Billing
  • Notifications
  • API keys (upgrade required)
<ul class="list-group w-100" style="max-width: 24rem;">
  <li class="list-group__item">Profile</li>
  <li class="list-group__item" data-state="active" aria-current="true">Billing</li>
  <li class="list-group__item">Notifications</li>
  <li class="list-group__item" aria-disabled="true">API keys (upgrade required)</li>
</ul>

Swap <li> for <a> or <button> and the rows pick up hover and focus automatically. No opt-in class needed. Mark the current page with aria-current="page". A first-child <svg> or <i> pins as a leading icon at --list-group-item-icon-size.

<div class="list-group w-100" style="max-width: 24rem;">
  <a href="#" class="list-group__item"><i data-lucide="layout-dashboard"></i>Dashboard</a>
  <a href="#" class="list-group__item"><i data-lucide="folder-kanban"></i>Projects</a>
  <a href="#" class="list-group__item" aria-current="page"><i data-lucide="users"></i>Team</a>
  <a href="#" class="list-group__item"><i data-lucide="settings"></i>Settings</a>
  <a href="#" class="list-group__item" aria-disabled="true"><i data-lucide="lock"></i>Audit log</a>
</div>

Flush

Add .list-group--flush to drop the outer border and radius. Rows sit edge to edge with a divider between them. Useful inside popovers, sidebar panels, or anywhere that already owns a frame.

  • Acme Corp
  • Nimbus Labs
  • Pinecone Studio
  • Westwind Holdings
<ul class="list-group list-group--flush w-100" style="max-width: 24rem;">
  <li class="list-group__item">Acme Corp</li>
  <li class="list-group__item">Nimbus Labs</li>
  <li class="list-group__item">Pinecone Studio</li>
  <li class="list-group__item">Westwind Holdings</li>
</ul>

Numbered

Drop .list-group--numbered on the wrapper and use an <ol>. A CSS counter stamps the prefix; nest another numbered list and the counter cascades.

  1. Install the CLI
  2. Authenticate with your account
  3. Initialize a new project
  4. Deploy to production
<ol class="list-group list-group--numbered w-100" style="max-width: 24rem;">
  <li class="list-group__item">Install the CLI</li>
  <li class="list-group__item">Authenticate with your account</li>
  <li class="list-group__item">Initialize a new project</li>
  <li class="list-group__item">Deploy to production</li>
</ol>

Horizontal

Use .list-group--horizontal to lay rows side by side instead of stacked. Add a breakpoint suffix (-sm, -md, -lg, -xl, -xxl) to switch to row layout above that width.

  • All
  • Open
  • In review
  • Merged
  • Closed
<ul class="list-group list-group--horizontal">
  <li class="list-group__item" data-state="active" aria-current="true">All</li>
  <li class="list-group__item">Open</li>
  <li class="list-group__item">In review</li>
  <li class="list-group__item">Merged</li>
  <li class="list-group__item">Closed</li>
</ul>

With badge

Push a count or status to the trailing edge with a span set to flex: 1 on the label. Reads as a counter row.

  • Inbox 14
  • Pull requests 3
  • Mentions 2
  • Drafts 5
  • Archived 128
<ul class="list-group w-100" style="max-width: 24rem;">
  <li class="list-group__item">
    <i data-lucide="inbox"></i>
    <span class="flex-fill">Inbox</span>
    <span class="badge badge--primary">14</span>
  </li>
  <li class="list-group__item">
    <i data-lucide="git-pull-request"></i>
    <span class="flex-fill">Pull requests</span>
    <span class="badge badge--primary">3</span>
  </li>
  <li class="list-group__item">
    <i data-lucide="at-sign"></i>
    <span class="flex-fill">Mentions</span>
    <span class="badge badge--primary">2</span>
  </li>
  <li class="list-group__item">
    <i data-lucide="file-text"></i>
    <span class="flex-fill">Drafts</span>
    <span class="badge badge--soft">5</span>
  </li>
  <li class="list-group__item">
    <i data-lucide="archive"></i>
    <span class="flex-fill">Archived</span>
    <span class="badge badge--soft">128</span>
  </li>
</ul>

Contextual variants

Tint a row with .list-group__item--{intent} for status messaging. Same five intents as .alert and .table__row--{intent}. Add .list-group__item--neutral for a quiet fill that tracks the interactional neutral.

  • New version v3.2 is ready to deploy
  • Nightly database backup completed
  • Maintenance window scheduled for Friday 02:00 UTC
  • API requests approaching the hourly limit
  • Payment processor returned a 503, retrying
  • Cron job last ran 2 hours ago
<ul class="list-group w-100" style="max-width: 28rem;">
  <li class="list-group__item list-group__item--primary">New version v3.2 is ready to deploy</li>
  <li class="list-group__item list-group__item--success">Nightly database backup completed</li>
  <li class="list-group__item list-group__item--info">Maintenance window scheduled for Friday 02:00 UTC</li>
  <li class="list-group__item list-group__item--warning">API requests approaching the hourly limit</li>
  <li class="list-group__item list-group__item--danger">Payment processor returned a 503, retrying</li>
  <li class="list-group__item list-group__item--neutral">Cron job last ran 2 hours ago</li>
</ul>

Custom content

A row can hold whatever you need. A title, a subhead, a timestamp. Override align-items and flex-direction on the row when content stacks vertically.

<div class="list-group w-100" style="max-width: 32rem;">
  <a href="#" class="list-group__item flex-column align-items-stretch gap-1" aria-current="page">
    <div class="d-flex justify-content-between gap-2">
      <span class="fw-semibold">Mariam Saidova opened a pull request</span>
      <small>just now</small>
    </div>
    <p>Refactor the checkout flow into smaller routes so the bundle splits cleanly on Stripe success / cancel.</p>
    <small>#1248 · billing · 6 files changed</small>
  </a>
  <a href="#" class="list-group__item flex-column align-items-stretch gap-1">
    <div class="d-flex justify-content-between gap-2">
      <span class="fw-semibold">Production deploy succeeded</span>
      <small class="text-muted-foreground">14 minutes ago</small>
    </div>
    <p>Release <code>v3.1.7</code> rolled out to the EU and US regions. Edge cache warmed in 38 seconds.</p>
    <small class="text-muted-foreground">@nauval · deploys</small>
  </a>
  <a href="#" class="list-group__item flex-column align-items-stretch gap-1">
    <div class="d-flex justify-content-between gap-2">
      <span class="fw-semibold">Hideo Tanaka left a review on #1241</span>
      <small class="text-muted-foreground">1 hour ago</small>
    </div>
    <p>Two small comments on the new <code>useReducedMotion</code> hook, otherwise looks good to merge.</p>
    <small class="text-muted-foreground">design system · 2 comments</small>
  </a>
</div>

Contacts

Compose a chat-style contact row from an avatar, a two-line label, and a trailing timestamp + unread counter. Each row is an <a> so the whole strip is clickable.

<div class="list-group w-100" style="max-width: 32rem;">
  <a href="#" class="list-group__item gap-3 py-3" aria-current="page">
    <img class="flex-shrink-0" src="https://i.pravatar.cc/80?img=12" alt="" width="40" height="40" style="border-radius: 50%;">
    <div class="flex-fill" style="min-width: 0;">
      <div class="fw-medium">Mariam Saidova</div>
      <div class="text-muted-foreground fs-2 text-truncate">Sure, I'll push the branch in a few minutes. Just running the tests one more time…</div>
    </div>
    <div class="d-flex flex-column align-items-end gap-1 flex-shrink-0">
      <small>12:04</small>
      <span class="badge badge--primary">3</span>
    </div>
  </a>
  <a href="#" class="list-group__item gap-3 py-3">
    <img class="flex-shrink-0" src="https://i.pravatar.cc/80?img=33" alt="" width="40" height="40" style="border-radius: 50%;">
    <div class="flex-fill" style="min-width: 0;">
      <div class="fw-medium">Hideo Tanaka</div>
      <div class="text-muted-foreground fs-2 text-truncate">Design review notes are in the Figma comments. Let me know what you think.</div>
    </div>
    <div class="d-flex flex-column align-items-end gap-1 flex-shrink-0">
      <small class="text-muted-foreground">11:47</small>
      <span class="badge badge--primary">1</span>
    </div>
  </a>
  <a href="#" class="list-group__item gap-3 py-3">
    <img class="flex-shrink-0" src="https://i.pravatar.cc/80?img=47" alt="" width="40" height="40" style="border-radius: 50%;">
    <div class="flex-fill" style="min-width: 0;">
      <div class="fw-medium">Priya Ramanathan</div>
      <div class="text-muted-foreground fs-2 text-truncate">Coffee tomorrow? 9am at the usual place.</div>
    </div>
    <small class="text-muted-foreground flex-shrink-0">Yesterday</small>
  </a>
  <a href="#" class="list-group__item gap-3 py-3">
    <img class="flex-shrink-0" src="https://i.pravatar.cc/80?img=58" alt="" width="40" height="40" style="border-radius: 50%;">
    <div class="flex-fill" style="min-width: 0;">
      <div class="fw-medium">Tomás Reyes</div>
      <div class="text-muted-foreground fs-2 text-truncate">You: sounds good, let's do it.</div>
    </div>
    <small class="text-muted-foreground flex-shrink-0">Mon</small>
  </a>
</div>

Settings

Drop a .list-group as a direct child of a .card and the outer chrome auto-merges with the card. No --flush modifier needed. The card owns the frame, the list-group owns the inner rows.

Notifications
  • A weekly summary of activity across your projects.
  • Get a ping on this device when someone mentions you.
  • Used across the app and outgoing emails.
  • Shown on your profile and in mentions.
  • Delete workspace This permanently removes everything. Cannot be undone.
<div class="card w-100" style="max-width: 36rem;">
  <div class="card__header">
    Notifications
  </div>
  <ul class="list-group">
    <li class="list-group__item gap-3">
      <div class="flex-fill">
        <label class="form-label fw-medium mb-0" for="settingsEmail">Email digest</label>
        <small class="text-muted-foreground d-block">A weekly summary of activity across your projects.</small>
      </div>
      <label class="switch switch--lg flex-shrink-0">
        <input class="switch__input" type="checkbox" id="settingsEmail" checked>
      </label>
    </li>
    <li class="list-group__item gap-3">
      <div class="flex-fill">
        <label class="form-label fw-medium mb-0" for="settingsPush">Push notifications</label>
        <small class="text-muted-foreground d-block">Get a ping on this device when someone mentions you.</small>
      </div>
      <label class="switch switch--lg flex-shrink-0">
        <input class="switch__input" type="checkbox" id="settingsPush">
      </label>
    </li>
    <li class="list-group__item gap-3">
      <div class="flex-fill">
        <label class="form-label fw-medium mb-0" for="settingsLanguage">Language</label>
        <small class="text-muted-foreground d-block">Used across the app and outgoing emails.</small>
      </div>
      <select class="select select--sm flex-shrink-0" id="settingsLanguage" style="max-width: 10rem;">
        <option selected>English</option>
        <option>Bahasa Indonesia</option>
        <option>日本語</option>
        <option>Deutsch</option>
      </select>
    </li>
    <li class="list-group__item gap-3">
      <div class="flex-fill">
        <label class="form-label fw-medium mb-0" for="settingsHandle">Public handle</label>
        <small class="text-muted-foreground d-block">Shown on your profile and in mentions.</small>
      </div>
      <input type="text" class="input input--sm flex-shrink-0" id="settingsHandle" value="nauval" style="max-width: 10rem;">
    </li>
    <li class="list-group__item gap-3">
      <div class="flex-fill">
        <span class="fw-medium d-block">Delete workspace</span>
        <small class="text-muted-foreground d-block">This permanently removes everything. Cannot be undone.</small>
      </div>
      <button type="button" class="btn btn--outline btn--danger btn--sm flex-shrink-0">Delete</button>
    </li>
  </ul>
</div>

Payment methods

Stack saved methods as rows with a label, helper text, and a default-toggle on each. The switch promotes one card so the trailing edge stays uncluttered.

Visa ending in 4242
Expires 08 / 2028
Mastercard ending in 0119
Expires 03 / 2027
Bank transfer · BCA
Account ····3084
Wallet credit
28.40 available
<div class="list-group w-100" style="max-width: 36rem;">
  <div class="list-group__item gap-3">
    <div class="flex-fill">
      <div class="fw-medium">Visa ending in 4242</div>
      <small class="text-muted-foreground">Expires 08 / 2028</small>
    </div>
    <label class="switch switch--lg flex-shrink-0">
      <input class="switch__input" type="checkbox" id="payDefault1" checked aria-label="Default for billing">
    </label>
  </div>
  <div class="list-group__item gap-3">
    <div class="flex-fill">
      <div class="fw-medium">Mastercard ending in 0119</div>
      <small class="text-muted-foreground">Expires 03 / 2027</small>
    </div>
    <label class="switch switch--lg flex-shrink-0">
      <input class="switch__input" type="checkbox" id="payDefault2" aria-label="Default for billing">
    </label>
  </div>
  <div class="list-group__item gap-3">
    <div class="flex-fill">
      <div class="fw-medium">Bank transfer · BCA</div>
      <small class="text-muted-foreground">Account ····3084</small>
    </div>
    <label class="switch switch--lg flex-shrink-0">
      <input class="switch__input" type="checkbox" id="payDefault3" aria-label="Default for billing">
    </label>
  </div>
  <div class="list-group__item gap-3">
    <div class="flex-fill">
      <div class="fw-medium">Wallet credit</div>
      <small class="text-muted-foreground">
28.40 available</small> </div> <label class="switch switch--lg flex-shrink-0"> <input class="switch__input" type="checkbox" id="payDefault4" aria-label="Use wallet first"> </label> </div> </div>

Customization

Every .list-group reads from these component-scoped variables. Override on the root (or any wrapper) to retune a single instance.

VariableDefaultUse
--list-group-radiusvar(--st-list-group-radius, var(--st-radius))Outer corner radius.
--list-group-item-padding-ycalc(0.75rem * var(--st-density))Row vertical padding.
--list-group-item-padding-xcalc(1rem * var(--st-density))Row horizontal padding.
--list-group-item-gap0.75remGap between leading icon, label, and trailing slot.
--list-group-item-icon-size1remPinned size for a first-child <svg> / <i>.
--list-group-bgvar(--st-surface)Frame background.
--list-group-colorvar(--st-foreground)Row text color.
--list-group-border-colorvar(--st-border)Outer frame border.
--list-group-border-width1pxOuter frame border thickness.
--list-group-divider-colorvar(--st-border)Rule between adjacent rows.
--list-group-item-hover-bgvar(--st-accent)Hover background on interactive rows.
--list-group-item-hover-colorvar(--st-accent-foreground)Hover text color on interactive rows.
--list-group-item-active-bgvar(--st-highlight)Selected-row background ([data-state="active"] / aria-current).
--list-group-item-active-colorvar(--st-highlight-foreground)Selected-row text color.
--list-group-item-disabled-colorvar(--st-muted-foreground)Disabled-row text color.
--list-group-ringvar(--st-ring)Focus halo color on interactive rows.
--list-group-transitionbackground-color .12s ease, color .12s easeHover / selection state change. Zeroed under prefers-reduced-motion.