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 & 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>Links and buttons
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.
- Install the CLI
- Authenticate with your account
- Initialize a new project
- 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.
Refactor the checkout flow into smaller routes so the bundle splits cleanly on Stripe success / cancel.
#1248 · billing · 6 files changedRelease v3.1.7 rolled out to the EU and US regions. Edge cache warmed in 38 seconds.
Two small comments on the new useReducedMotion hook, otherwise looks good to merge.
<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.
-
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.
<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.
| Variable | Default | Use |
|---|---|---|
--list-group-radius | var(--st-list-group-radius, var(--st-radius)) | Outer corner radius. |
--list-group-item-padding-y | calc(0.75rem * var(--st-density)) | Row vertical padding. |
--list-group-item-padding-x | calc(1rem * var(--st-density)) | Row horizontal padding. |
--list-group-item-gap | 0.75rem | Gap between leading icon, label, and trailing slot. |
--list-group-item-icon-size | 1rem | Pinned size for a first-child <svg> / <i>. |
--list-group-bg | var(--st-surface) | Frame background. |
--list-group-color | var(--st-foreground) | Row text color. |
--list-group-border-color | var(--st-border) | Outer frame border. |
--list-group-border-width | 1px | Outer frame border thickness. |
--list-group-divider-color | var(--st-border) | Rule between adjacent rows. |
--list-group-item-hover-bg | var(--st-accent) | Hover background on interactive rows. |
--list-group-item-hover-color | var(--st-accent-foreground) | Hover text color on interactive rows. |
--list-group-item-active-bg | var(--st-highlight) | Selected-row background ([data-state="active"] / aria-current). |
--list-group-item-active-color | var(--st-highlight-foreground) | Selected-row text color. |
--list-group-item-disabled-color | var(--st-muted-foreground) | Disabled-row text color. |
--list-group-ring | var(--st-ring) | Focus halo color on interactive rows. |
--list-group-transition | background-color .12s ease, color .12s ease | Hover / selection state change. Zeroed under prefers-reduced-motion. |