List Group
Flexible and powerful lists for content display, navigation, and interactive item selection - with full keyboard navigation and accessibility support
Overview
List Group is a flexible component for rendering sequential content - from simple navigation links to rich content rows with icons, descriptive text, and trailing badges. Items can be made interactive with hover states, active selection, and full JavaScript control via E.listGroup().
Key Features
- CSS-Only - works with plain HTML classes, no JavaScript required for static lists
- JS API -
E.listGroup()enables single- and multi-select, keyboard navigation, and callbacks - Colour Variants - six semantic colour modifiers (primary, secondary, success, danger, warning, info)
- Size Variants - compact
.list-group-smand spacious.list-group-lg - Flush Mode -
.list-group-flushremoves outer border for embedding inside cards - Rich Items - icon, heading, descriptive text, and trailing area all built-in
- Full Accessibility - roving tabindex, ARIA roles, keyboard navigation (arrows, Home, End)
At a Glance
- Dashboard
- Profile New
- Settings
- Admin
- Deployment complete Production - 2 minutes ago Live
- High memory usage Server A - 87% used Warn
- Database connection lost DB-02 - 14 seconds ago Error
<div class="demo-row">
<div class="demo-col" style="max-width: 260px;">
<ul class="list-group">
<li class="list-group-item list-group-item-action active">
<span class="list-group-item-icon"><span data-icon="home"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Dashboard</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="user"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Profile</span>
</span>
<span class="list-group-item-trailing">
<span class="badge badge-primary">New</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="settings"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Settings</span>
</span>
</li>
<li class="list-group-item list-group-item-action disabled">
<span class="list-group-item-icon"><span data-icon="lock"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Admin</span>
</span>
</li>
</ul>
</div>
<div class="demo-col" style="max-width: 300px;">
<ul class="list-group">
<li class="list-group-item list-group-item-success">
<span class="list-group-item-content">
<span class="list-group-item-heading">Deployment complete</span>
<span class="list-group-item-text">Production - 2 minutes ago</span>
</span>
<span class="list-group-item-trailing"><span class="badge badge-success">Live</span></span>
</li>
<li class="list-group-item list-group-item-warning">
<span class="list-group-item-content">
<span class="list-group-item-heading">High memory usage</span>
<span class="list-group-item-text">Server A - 87% used</span>
</span>
<span class="list-group-item-trailing"><span class="badge badge-warning">Warn</span></span>
</li>
<li class="list-group-item list-group-item-danger">
<span class="list-group-item-content">
<span class="list-group-item-heading">Database connection lost</span>
<span class="list-group-item-text">DB-02 - 14 seconds ago</span>
</span>
<span class="list-group-item-trailing"><span class="badge badge-danger">Error</span></span>
</li>
</ul>
</div>
</div><!-- CSS-only static list -->
<ul class="list-group">
<li class="list-group-item active">Dashboard</li>
<li class="list-group-item">Profile</li>
<li class="list-group-item disabled">Admin</li>
</ul>
<!-- Interactive JS API -->
const lg = E.listGroup('#my-list', {
selectable: true,
onChange: (selected) => console.log(selected)
});
Getting Started
The simplest List Group is a <ul> with .list-group and <li> children with .list-group-item. No JavaScript needed.
- Apples
- Oranges
- Bananas
- Strawberries
<ul class="list-group" style="max-width: 320px;">
<li class="list-group-item">Apples</li>
<li class="list-group-item">Oranges</li>
<li class="list-group-item">Bananas</li>
<li class="list-group-item">Strawberries</li>
</ul><ul class="list-group">
<li class="list-group-item">Apples</li>
<li class="list-group-item">Oranges</li>
<li class="list-group-item">Bananas</li>
<li class="list-group-item">Strawberries</li>
</ul>
Add .list-group-item-action to make items interactive - cursor pointer and hover highlight:
- Dashboard
- Analytics
- Reports
- Archive
<ul class="list-group" style="max-width: 320px;">
<li class="list-group-item list-group-item-action active">Dashboard</li>
<li class="list-group-item list-group-item-action">Analytics</li>
<li class="list-group-item list-group-item-action">Reports</li>
<li class="list-group-item list-group-item-action disabled">Archive</li>
</ul><ul class="list-group">
<li class="list-group-item list-group-item-action active">Dashboard</li>
<li class="list-group-item list-group-item-action">Analytics</li>
<li class="list-group-item list-group-item-action">Reports</li>
<li class="list-group-item list-group-item-action disabled">Archive</li>
</ul>
Basic Variants
Plain Items
The default appearance - a clean bordered list.
- First item
- Second item
- Third item
- Fourth item
<ul class="list-group" style="max-width: 300px;">
<li class="list-group-item">First item</li>
<li class="list-group-item">Second item</li>
<li class="list-group-item">Third item</li>
<li class="list-group-item">Fourth item</li>
</ul><ul class="list-group">
<li class="list-group-item">First item</li>
<li class="list-group-item">Second item</li>
<li class="list-group-item">Third item</li>
</ul>
With Icons
Use .list-group-item-icon for a left-aligned icon or avatar area. Pair with <span data-icon="..."> and call I.scan().
- Inbox
- Sent
- Drafts
- Bin
<ul class="list-group" style="max-width: 300px;">
<li class="list-group-item">
<span class="list-group-item-icon"><span data-icon="mail"></span></span>
Inbox
</li>
<li class="list-group-item">
<span class="list-group-item-icon"><span data-icon="send"></span></span>
Sent
</li>
<li class="list-group-item">
<span class="list-group-item-icon"><span data-icon="edit"></span></span>
Drafts
</li>
<li class="list-group-item">
<span class="list-group-item-icon"><span data-icon="trash"></span></span>
Bin
</li>
</ul><ul class="list-group">
<li class="list-group-item">
<span class="list-group-item-icon">
<span data-icon="mail"></span>
</span>
Inbox
</li>
<li class="list-group-item">
<span class="list-group-item-icon">
<span data-icon="send"></span>
</span>
Sent
</li>
</ul>
With Content (Heading + Text + Trailing)
Combine .list-group-item-content, .list-group-item-heading, .list-group-item-text, and .list-group-item-trailing for rich rows.
- Alice Brennan alice@example.com Admin
- Bob Clarke bob@example.com Editor
- Carol Davies carol@example.com Viewer
<ul class="list-group" style="max-width: 400px;">
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="user"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Alice Brennan</span>
<span class="list-group-item-text">alice@example.com</span>
</span>
<span class="list-group-item-trailing">
<span class="badge badge-success">Admin</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="user"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Bob Clarke</span>
<span class="list-group-item-text">bob@example.com</span>
</span>
<span class="list-group-item-trailing">
<span class="badge badge-secondary">Editor</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="user"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Carol Davies</span>
<span class="list-group-item-text">carol@example.com</span>
</span>
<span class="list-group-item-trailing">
<span class="badge badge-secondary">Viewer</span>
</span>
</li>
</ul><li class="list-group-item list-group-item-action">
<span class="list-group-item-icon">
<span data-icon="user"></span>
</span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Alice Brennan</span>
<span class="list-group-item-text">alice@example.com</span>
</span>
<span class="list-group-item-trailing">
<span class="badge badge-success">Admin</span>
</span>
</li>
Colour Variants
Add a semantic colour class to any .list-group-item - it applies a left border accent and a lightly tinted background.
- Primary .list-group-item-primary
- Secondary .list-group-item-secondary
- Success .list-group-item-success
- Danger .list-group-item-danger
- Warning .list-group-item-warning
- Info .list-group-item-info
<ul class="list-group" style="max-width: 420px;">
<li class="list-group-item list-group-item-primary">
<span class="list-group-item-content">
<span class="list-group-item-heading">Primary</span>
<span class="list-group-item-text">.list-group-item-primary</span>
</span>
</li>
<li class="list-group-item list-group-item-secondary">
<span class="list-group-item-content">
<span class="list-group-item-heading">Secondary</span>
<span class="list-group-item-text">.list-group-item-secondary</span>
</span>
</li>
<li class="list-group-item list-group-item-success">
<span class="list-group-item-content">
<span class="list-group-item-heading">Success</span>
<span class="list-group-item-text">.list-group-item-success</span>
</span>
</li>
<li class="list-group-item list-group-item-danger">
<span class="list-group-item-content">
<span class="list-group-item-heading">Danger</span>
<span class="list-group-item-text">.list-group-item-danger</span>
</span>
</li>
<li class="list-group-item list-group-item-warning">
<span class="list-group-item-content">
<span class="list-group-item-heading">Warning</span>
<span class="list-group-item-text">.list-group-item-warning</span>
</span>
</li>
<li class="list-group-item list-group-item-info">
<span class="list-group-item-content">
<span class="list-group-item-heading">Info</span>
<span class="list-group-item-text">.list-group-item-info</span>
</span>
</li>
</ul><ul class="list-group">
<li class="list-group-item list-group-item-primary">Primary</li>
<li class="list-group-item list-group-item-secondary">Secondary</li>
<li class="list-group-item list-group-item-success">Success</li>
<li class="list-group-item list-group-item-danger">Danger</li>
<li class="list-group-item list-group-item-warning">Warning</li>
<li class="list-group-item list-group-item-info">Info</li>
</ul>
Size Variants
Apply .list-group-sm for a compact list or .list-group-lg for extra padding and larger text - useful in sidebars or content-heavy interfaces.
Small - .list-group-sm
- Home
- Products
- About
- Contact
Default
- Home
- Products
- About
- Contact
Large - .list-group-lg
- Home
- Products
- About
- Contact
<div class="demo-row mb-6"><div class="demo-col">
<p class="demo-label">Small - .list-group-sm</p>
<ul class="list-group list-group-sm">
<li class="list-group-item list-group-item-action">Home</li>
<li class="list-group-item list-group-item-action active">Products</li>
<li class="list-group-item list-group-item-action">About</li>
<li class="list-group-item list-group-item-action">Contact</li>
</ul>
</div>
<div class="demo-col">
<p class="demo-label">Default</p>
<ul class="list-group">
<li class="list-group-item list-group-item-action">Home</li>
<li class="list-group-item list-group-item-action active">Products</li>
<li class="list-group-item list-group-item-action">About</li>
<li class="list-group-item list-group-item-action">Contact</li>
</ul>
</div>
<div class="demo-col">
<p class="demo-label">Large - .list-group-lg</p>
<ul class="list-group list-group-lg">
<li class="list-group-item list-group-item-action">Home</li>
<li class="list-group-item list-group-item-action active">Products</li>
<li class="list-group-item list-group-item-action">About</li>
<li class="list-group-item list-group-item-action">Contact</li>
</ul>
</div>
</div><!-- Compact -->
<ul class="list-group list-group-sm"> ... </ul>
<!-- Default -->
<ul class="list-group"> ... </ul>
<!-- Spacious -->
<ul class="list-group list-group-lg"> ... </ul>
Flush
.list-group-flush removes the outer border and border-radius so the list blends seamlessly when embedded inside a card. A top border is added to the first item to maintain separation from card content above it.
- New comment on your post 5 minutes ago
- You have a new message 1 hour ago
- Deployment succeeded 3 hours ago
<div class="card" style="max-width: 360px;">
<div class="card-header">
<strong>Recent Notifications</strong>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="bell"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">New comment on your post</span>
<span class="list-group-item-text">5 minutes ago</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="mail"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">You have a new message</span>
<span class="list-group-item-text">1 hour ago</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="check-circle"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Deployment succeeded</span>
<span class="list-group-item-text">3 hours ago</span>
</span>
</li>
</ul>
</div><div class="card">
<div class="card-header">Recent Notifications</div>
<ul class="list-group list-group-flush">
<li class="list-group-item list-group-item-action">
<span class="list-group-item-content">
<span class="list-group-item-heading">New comment on your post</span>
<span class="list-group-item-text">5 minutes ago</span>
</span>
</li>
...
</ul>
</div>
Numbered
Add .list-group-numbered to an <ol> to prepend auto-incrementing numbers to each item via CSS counters - no JavaScript required. Works with all size and colour variants.
Basic numbered
- Cras justo odio
- Dapibus ac facilisis in
- Morbi leo risus
- Porta ac consectetur ac
- Vestibulum at eros
Numbered with badges
- Inbox 14
- Sent 2
- Starred 8
- Spam 99+
<div class="demo-row">
<div class="demo-col">
<p class="text-sm text-muted mb-2">Basic numbered</p>
<ol class="list-group list-group-numbered">
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
</ol>
</div>
<div class="demo-col">
<p class="text-sm text-muted mb-2">Numbered with badges</p>
<ol class="list-group list-group-numbered">
<li class="list-group-item">
Inbox
<span class="list-group-item-trailing"><span class="badge badge-primary">14</span></span>
</li>
<li class="list-group-item">
Sent
<span class="list-group-item-trailing"><span class="badge badge-secondary">2</span></span>
</li>
<li class="list-group-item">
Starred
<span class="list-group-item-trailing"><span class="badge badge-warning">8</span></span>
</li>
<li class="list-group-item">
Spam
<span class="list-group-item-trailing"><span class="badge badge-danger">99+</span></span>
</li>
</ol>
</div>
</div><ol class="list-group list-group-numbered">
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
</ol>
<!-- With badges -->
<ol class="list-group list-group-numbered">
<li class="list-group-item">
Inbox
<span class="list-group-item-trailing"><span class="badge badge-primary">14</span></span>
</li>
</ol>
Horizontal
Change the layout from vertical to horizontal with .list-group-horizontal. Use the responsive variants - .list-group-horizontal-sm, -md, -lg, -xl - to switch to horizontal only above a given breakpoint.
Always horizontal
- An item
- Active
- A third item
- A fourth item
Horizontal above md (768px), stacked below
- One
- Two
- Three
<div class="mb-4">
<p class="text-sm text-muted mb-2">Always horizontal</p>
<ul class="list-group list-group-horizontal">
<li class="list-group-item">An item</li>
<li class="list-group-item active">Active</li>
<li class="list-group-item">A third item</li>
<li class="list-group-item">A fourth item</li>
</ul>
</div>
<div class="mb-4">
<p class="text-sm text-muted mb-2">Horizontal above <code>md</code> (768px), stacked below</p>
<ul class="list-group list-group-horizontal-md">
<li class="list-group-item">One</li>
<li class="list-group-item">Two</li>
<li class="list-group-item">Three</li>
</ul>
</div>
<div>
<p class="text-sm text-muted mb-2">Horizontal with action items</p>
<div class="list-group list-group-horizontal">
<a href="#" class="list-group-item list-group-item-action active">Home</a>
<a href="#" class="list-group-item list-group-item-action">Profile</a>
<a href="#" class="list-group-item list-group-item-action">Settings</a>
<a href="#" class="list-group-item list-group-item-action disabled">Help</a>
</div>
</div><!-- Always horizontal -->
<ul class="list-group list-group-horizontal">
<li class="list-group-item">An item</li>
<li class="list-group-item active">Active</li>
<li class="list-group-item">A third item</li>
</ul>
<!-- Horizontal above md breakpoint (768px) -->
<ul class="list-group list-group-horizontal-md">
<li class="list-group-item">One</li>
<li class="list-group-item">Two</li>
<li class="list-group-item">Three</li>
</ul>
<!-- Available breakpoints: -sm (576px), -md (768px), -lg (992px), -xl (1200px) -->
Interactive - JavaScript API
Use E.listGroup(selector, options) to enable selectable behaviour, keyboard navigation, and callbacks. Set selectable: true and add .list-group-item-action to items.
Single Select
Clicking an item selects it and deselects any previously selected item. The onChange callback receives an array of the currently selected DOM elements.
- Overview
- Analytics
- Reports
- Billing (disabled)
Selected item:
<div class="flex gap-6 flex-wrap items-start">
<ul id="single-select-list" class="list-group" style="min-width: 220px; max-width: 260px;">
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="bar-chart"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Overview</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="trending-up"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Analytics</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="file-text"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Reports</span>
</span>
</li>
<li class="list-group-item list-group-item-action disabled">
<span class="list-group-item-icon"><span data-icon="lock"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Billing (disabled)</span>
</span>
</li>
</ul>
<div style="flex: 1; min-width: 180px;">
<p class="text-sm text-muted mb-2">Selected item:</p>
<div id="single-select-output" class="demo-output">None selected</div>
<div class="flex gap-2 flex-wrap mt-3">
<button type="button" id="single-select-btn" class="btn btn-sm btn-primary">Select Analytics</button>
<button type="button" id="single-deselect-btn" class="btn btn-sm btn-outline">Deselect All</button>
</div>
</div>
</div>/**
* Single Select - from public/showcase/elements/list-group.
*/
export function mount(root) {
// --------------------------
// Section 7: Single Select
// --------------------------
const singleLg = E.listGroup(root.querySelector('#single-select-list'), {
selectable: true,
onChange: (selected) => {
const label = selected[0]
? selected[0].querySelector('.list-group-item-heading').textContent
: 'None selected';
$(root).find('#single-select-output').text(label);
}
});
$(root).find('#single-select-btn').on('click', () => {
singleLg.select(1); // Select "Analytics" (index 1)
const sel = singleLg.getSelected();
const label = sel[0]
? sel[0].querySelector('.list-group-item-heading').textContent
: 'None selected';
$(root).find('#single-select-output').text(label);
});
$(root).find('#single-deselect-btn').on('click', () => {
singleLg.deselectAll();
$(root).find('#single-select-output').text('None selected');
});
}const lg = E.listGroup('#my-list', {
selectable: true,
onChange: (selected) => {
const label = selected[0]
? selected[0].querySelector('.list-group-item-heading').textContent
: 'None selected';
$('#output').text(label);
}
});
// Programmatic selection
lg.select(1); // Select item at index 1
lg.deselect(1); // Deselect item at index 1
lg.deselectAll(); // Clear all selections
lg.getSelected(); // → [HTMLElement, ...]
Multi-Select
Set multiSelect: true alongside selectable: true to allow multiple items to be active simultaneously. The list container receives aria-multiselectable="true" automatically.
- Design
- Development
- Project Management
- Analytics
- HR & Hiring
Selected categories:
<div class="flex gap-6 flex-wrap items-start">
<ul id="multi-select-list" class="list-group" style="min-width: 240px; max-width: 280px;">
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="tag"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Design</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="code"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Development</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="clipboard"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Project Management</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="bar-chart"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Analytics</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="users"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">HR & Hiring</span>
</span>
</li>
</ul>
<div style="flex: 1; min-width: 180px;">
<p class="text-sm text-muted mb-2">Selected categories:</p>
<div id="multi-select-output" class="demo-output">None selected</div>
<div class="flex gap-2 flex-wrap mt-3">
<button type="button" id="multi-select-all-btn" class="btn btn-sm btn-primary">Select All</button>
<button type="button" id="multi-deselect-all-btn" class="btn btn-sm btn-outline">Clear All</button>
</div>
</div>
</div>/**
* Multi-Select - from public/showcase/elements/list-group.
*/
export function mount(root) {
// --------------------------
// Section 8: Multi-Select
// --------------------------
const updateMultiOutput = (selected) => {
const labels = selected.map(el =>
el.querySelector('.list-group-item-heading').textContent
);
$(root).find('#multi-select-output').text(
labels.length ? labels.join(', ') : 'None selected'
);
};
const multiLg = E.listGroup(root.querySelector('#multi-select-list'), {
selectable: true,
multiSelect: true,
onChange: updateMultiOutput
});
$(root).find('#multi-select-all-btn').on('click', () => {
multiLg.selectAll();
updateMultiOutput(multiLg.getSelected());
});
$(root).find('#multi-deselect-all-btn').on('click', () => {
multiLg.deselectAll();
updateMultiOutput(multiLg.getSelected());
});
}const lg = E.listGroup('#my-list', {
selectable: true,
multiSelect: true, // Allow multiple active items
onChange: (selected) => {
const labels = selected.map(el =>
el.querySelector('.list-group-item-heading').textContent
);
console.log('Selected:', labels);
}
});
lg.selectAll(); // Select every non-disabled item
lg.deselectAll(); // Clear all selections
lg.toggle(2); // Toggle item at index 2
Tutorial - Settings Panel
Build a settings panel where the left-hand List Group acts as a navigation menu and the right panel updates to show the relevant settings on each selection.
Step 1: Create the HTML structure
<div class="settings-layout">
<ul id="settings-nav" class="list-group list-group-sm settings-sidebar">
<li class="list-group-item list-group-item-action active">
<span class="list-group-item-icon"><span data-icon="settings"></span></span>
<span class="list-group-item-heading">General</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="lock"></span></span>
<span class="list-group-item-heading">Security</span>
</li>
...
</ul>
<div id="settings-panel" class="settings-panel"></div>
</div>
Step 2: Initialise with onChange callback
const panels = {
0: { title: 'General', body: 'Manage your language, timezone, and display preferences.' },
1: { title: 'Security', body: 'Update your password and configure two-factor authentication.' },
2: { title: 'Notifications', body: 'Choose how and when you receive alerts.' },
3: { title: 'Privacy', body: 'Control data sharing and visibility settings.' },
};
const settingsNav = E.listGroup('#settings-nav', {
selectable: true,
onChange: (selected) => {
const index = lg.getItems().indexOf(selected[0]);
const p = panels[index];
$('#settings-panel').html(
`<h4>${p.title}</h4><p>${p.body}</p>`
);
}
});
Step 3: Live Demo
General
Manage your language, timezone, and display preferences.
<div class="settings-layout">
<ul id="tutorial-settings-nav" class="list-group list-group-sm settings-sidebar">
<li class="list-group-item list-group-item-action active">
<span class="list-group-item-icon"><span data-icon="settings"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">General</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="lock"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Security</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="bell"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Notifications</span>
</span>
</li>
<li class="list-group-item list-group-item-action">
<span class="list-group-item-icon"><span data-icon="eye"></span></span>
<span class="list-group-item-content">
<span class="list-group-item-heading">Privacy</span>
</span>
</li>
</ul>
<div id="tutorial-settings-panel" class="settings-panel">
<h4 class="font-semibold mb-2" data-bind-text="panel.value.title">General</h4>
<p class="text-muted" data-bind-text="panel.value.body">Manage your language, timezone, and display preferences.</p>
</div>
</div>/**
* Step 3: Live Demo - from public/showcase/elements/list-group.
*/
export function mount(root) {
// --------------------------
// Section 9: Tutorial - Settings Panel
// --------------------------
const settingsPanels = [
{ title: 'General', body: 'Manage your language, timezone, and display preferences.' },
{ title: 'Security', body: 'Update your password and configure two-factor authentication.' },
{ title: 'Notifications', body: 'Choose how and when you receive email, SMS, and push alerts.' },
{ title: 'Privacy', body: 'Control data sharing, visibility settings, and download your data.' },
];
// The panel shows whichever entry this observable holds (data-bind-text in demo.html)
const panel = M.observable(settingsPanels[0]);
const handle = M.applyBindings({panel}, root.querySelector('#tutorial-settings-panel'));
const tutorialLg = E.listGroup(root.querySelector('#tutorial-settings-nav'), {
selectable: true,
onChange: (selected) => {
if (!selected.length) return;
const index = tutorialLg.getItems().indexOf(selected[0]);
const p = settingsPanels[index];
if (p) panel.value = p;
}
});
// Pre-select General on load
tutorialLg.select(0);
return () => {
handle.dispose();
tutorialLg?.destroy?.();
};
}JavaScript API
Initialisation
const lg = E.listGroup('#my-list', {
selectable: true, // Enable click-to-select behaviour
multiSelect: false, // Allow multiple active items (default: false)
activeClass: 'active', // CSS class for selected state (default: 'active')
disabledClass: 'disabled',// CSS class for disabled items (default: 'disabled')
itemSelector: '.list-group-item', // Item query selector (default: '.list-group-item')
keyboard: true, // Enable arrow-key navigation (default: true)
loop: true, // Wrap focus at list boundaries (default: true)
focusOnInit: false, // Focus first enabled item on init (default: false)
onChange: (selected, event) => {}, // Fires on selection change
onSelect: (item, index, event) => {}, // Fires when item selected
onDeselect:(item, index, event) => {}, // Fires when item deselected
});
Selection Methods
lg.select(index) // Select item at index (fires callbacks)
lg.deselect(index) // Deselect item at index (fires callbacks)
lg.toggle(index) // Toggle selection at index
lg.selectAll() // Select all non-disabled items (multiSelect mode only)
lg.deselectAll() // Deselect all items
lg.getSelected() // → HTMLElement[] of currently active items
lg.isSelected(index) // → boolean
Item Methods
lg.getItems() // → HTMLElement[] - all items
lg.getItem(index) // → HTMLElement | null
lg.enable(index) // Remove disabled class and aria-disabled
lg.disable(index) // Add disabled class and aria-disabled
lg.refresh() // Re-query items from DOM (use after dynamic changes)
lg.destroy() // Remove event listeners and clean up ARIA attributes
Keyboard Navigation
When keyboard navigation is enabled (default), users can move between items without using the mouse. The component uses a roving tabindex pattern - only the focused item is in the tab order at any time.
| Key | Action |
|---|---|
Tab |
Move focus into (or away from) the list |
↓ / → |
Move focus to the next enabled item |
↑ / ← |
Move focus to the previous enabled item |
Home |
Move focus to the first enabled item |
End |
Move focus to the last enabled item |
Enter / Space |
Select (or toggle) the focused item (when selectable: true) |
// Disable keyboard navigation
const lg = E.listGroup('#my-list', {
selectable: true,
keyboard: false // Turn off arrow-key navigation
});
// Disable looping at boundaries
const lg = E.listGroup('#my-list', {
selectable: true,
loop: false // Focus stops at first / last item
});
Accessibility
Automatic ARIA Attributes
When selectable: true is set, the JS API manages ARIA attributes automatically - no manual aria-* attributes needed in your HTML.
| Element | Attribute | Value |
|---|---|---|
.list-group |
role |
"listbox" |
.list-group (multi) |
aria-multiselectable |
"true" |
.list-group-item |
aria-selected |
"true" or "false" |
.list-group-item.disabled |
aria-disabled |
"true" |
.list-group-item |
tabindex |
Roving: 0 (focused) or -1 |
CSS-Only Guidance
For CSS-only (non-JS) lists, add ARIA attributes yourself when semantics matter:
<!-- Navigation list - use <nav> + <ul> -->
<nav aria-label="Main navigation">
<ul class="list-group">
<li class="list-group-item">
<a href="/dashboard" aria-current="page">Dashboard</a>
</li>
<li class="list-group-item"><a href="/reports">Reports</a></li>
</ul>
</nav>
<!-- Content list - plain <ul> is fine -->
<ul class="list-group" aria-label="Notifications">
<li class="list-group-item">...</li>
</ul>
Screen Reader Notes
- In
selectablemode the list is announced as a listbox; each item as an option. - Selected state is announced via
aria-selected, so avoid relying on colour alone. - Icon-only list items should include visually-hidden text or an
aria-labelon the item for context. - Disabled items receive
aria-disabled="true"andpointer-events: none, so keyboard focus skips them automatically.
Config Engine
List Group is supported in the declarative config engine via $.setup(), allowing you to initialise the component without writing imperative JavaScript.
$.setup({
'#my-list': {
component: 'listGroup',
options: {
selectable: true,
multiSelect: false,
onChange: (selected) => {
console.log('Selection changed:', selected.length, 'item(s) selected');
}
}
}
});
Live Config Engine Demo
- Initialised via Config Engine
- Click to select an item
- Works exactly like JS API
<ul id="config-engine-list" class="list-group" style="max-width: 280px;">
<li class="list-group-item list-group-item-action">Initialised via Config Engine</li>
<li class="list-group-item list-group-item-action">Click to select an item</li>
<li class="list-group-item list-group-item-action">Works exactly like JS API</li>
</ul>
<div id="config-engine-output" class="demo-output mt-3" style="max-width: 280px;">No selection</div>/**
* Live Config Engine Demo - from public/showcase/elements/list-group.
*/
export function mount(root) {
// --------------------------
// Section 13: Config Engine
// --------------------------
$.setup({
'#config-engine-list': {
component: 'listGroup',
options: {
selectable: true,
onChange: (selected) => {
const label = selected[0]
? selected[0].textContent.trim()
: 'No selection';
$(root).find('#config-engine-output').text(label);
}
}
}
});
}CSS Customisation
List Group uses standard Domma design tokens. Override these variables globally or scoped to a specific container.
| Variable | Controls |
|---|---|
--dm-border |
Outer border colour of the list group |
--dm-border-light |
Separator colour between items |
--dm-surface |
Item background colour |
--dm-background-alt |
Item hover background colour |
--dm-border-focus |
Focus ring colour (inset box-shadow) |
--dm-primary |
Active item background colour |
--dm-radius-md |
Border radius of the list group container |
--dm-space-3 / --dm-space-4 |
Default item padding |
/* Tighter separators and softer border */
#my-list {
--dm-border: rgba(0, 0, 0, 0.08);
--dm-border-light: rgba(0, 0, 0, 0.04);
}
/* Custom active colour (override primary) */
#my-list .list-group-item.active {
background-color: #16a34a; /* Green */
border-bottom-color: #15803d;
}
/* Borderless items with rounded container */
#my-list {
--dm-radius-md: 1rem;
}
#my-list .list-group-item {
border-bottom: none;
margin-bottom: 2px;
}
Full CSS Customisation Cheat-Sheet →
API Reference
Options
| Option | Type | Default | Description |
|---|---|---|---|
selectable |
boolean | false |
Enable click-to-select behaviour and ARIA listbox roles |
multiSelect |
boolean | false |
Allow more than one item to be active at a time |
activeClass |
string | 'active' |
CSS class applied to selected items |
disabledClass |
string | 'disabled' |
CSS class that marks items as non-interactive |
itemSelector |
string | '.list-group-item' |
Query selector used to discover child items |
keyboard |
boolean | true |
Enable arrow-key / Home / End navigation |
loop |
boolean | true |
Wrap focus when navigating past first / last item |
focusOnInit |
boolean | false |
Automatically focus the first enabled item on initialisation |
onChange |
function | null |
(selected: HTMLElement[], event) => void - fires on any selection change |
onSelect |
function | null |
(item: HTMLElement, index: number, event) => void - fires when an item is selected |
onDeselect |
function | null |
(item: HTMLElement, index: number, event) => void - fires when an item is deselected |
Methods
| Method | Returns | Description |
|---|---|---|
select(index) |
this | Select item at index; fires onSelect and onChange |
deselect(index) |
this | Deselect item at index; fires onDeselect and onChange |
toggle(index) |
this | Toggle selection at index |
selectAll() |
this | Select all non-disabled items (multiSelect mode only) |
deselectAll() |
this | Deselect all items; fires callbacks |
getSelected() |
HTMLElement[] | Returns array of currently active items |
isSelected(index) |
boolean | Whether item at index is currently selected |
getItems() |
HTMLElement[] | Returns all item elements as a shallow copy |
getItem(index) |
HTMLElement|null | Returns single item element at index |
enable(index) |
this | Remove disabled class and aria-disabled from item |
disable(index) |
this | Add disabled class and aria-disabled to item |
refresh() |
this | Re-query items from DOM; call after dynamic content changes |
destroy() |
void | Remove event listeners and clean up all ARIA attributes |
CSS Classes
| Class | Description |
|---|---|
.list-group |
Container - flex column, rounded border, overflow hidden |
.list-group-flush |
Modifier - remove outer border and radius (for card embedding) |
.list-group-sm |
Modifier - reduced padding and smaller font size |
.list-group-lg |
Modifier - increased padding and base font size |
.list-group-item |
Item - flex row, vertically centred, bottom separator |
.list-group-item-action |
Modifier - pointer cursor and hover highlight |
.list-group-item.active |
Selected/active state - primary background, white text |
.list-group-item.disabled |
Non-interactive state - dimmed, no pointer events |
.list-group-item-icon |
Left-aligned icon/avatar slot |
.list-group-item-content |
Flex-grow text wrapper |
.list-group-item-heading |
Primary text - semi-bold, truncated with ellipsis |
.list-group-item-text |
Secondary/muted text - smaller, truncated |
.list-group-item-trailing |
Right-aligned area (badges, timestamps, icons) |
.list-group-item-primary |
Colour variant - left border + tinted background |
.list-group-item-secondary |
Colour variant |
.list-group-item-success |
Colour variant |
.list-group-item-danger |
Colour variant |
.list-group-item-warning |
Colour variant |
.list-group-item-info |
Colour variant |
Related Elements
Tabs
Horizontally organised panels - use when content fits across a top bar rather than a side list
Accordion
Collapsible sections - ideal when each item has a large content area to reveal or hide
Button Group
Inline radio/checkbox groups - better for compact option sets where a list layout is not needed
<div class="grid grid-cols-1 gap-4 grid-auto-fit-250"><a href="/showcase/elements/tabs" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Tabs</h3>
<p class="card-text">Horizontally organised panels - use when content fits across a top bar rather than a side list</p>
</div>
</a>
<a href="/showcase/elements/accordion" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Accordion</h3>
<p class="card-text">Collapsible sections - ideal when each item has a large content area to reveal or hide</p>
</div>
</a>
<a href="/showcase/elements/button-group" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Button Group</h3>
<p class="card-text">Inline radio/checkbox groups - better for compact option sets where a list layout is not needed</p>
</div>
</a>
</div>