Lists
Organize content with Material lists, selection controls, and submission checklists.
Standard and segmented lists with flexible text, visual, and control slots, based on Material 3 Lists.
List styles are included in Materialize CSS. Plain lists need no JavaScript; import initListChecklist from kmaterialize for checklist attribution and progress. Existing Collections remain available.
One, two, and three lines
Use a headline with optional supporting text, leading visuals, and trailing metadata. Rows grow with their content; the three-line variant aligns content to the top.
- folder Project files 12 items
- description Review notesUpdated this morning PDF
-
movie
Sequence deliveryFinal renders and review notes.
Ready for the next production review. Today
<ul class="list" aria-label="Files">
<!-- One line: minimum height 56px. -->
<li class="list-item">
<span class="list-leading">
<i class="material-icons" aria-hidden="true">folder</i>
</span>
<span class="list-content"><span class="list-headline">Project files</span></span>
<span class="list-trailing">12 items</span>
</li>
<!-- Two lines: minimum height 72px. -->
<li class="list-item list-two-line">
<span class="list-leading">
<i class="material-icons" aria-hidden="true">description</i>
</span>
<span class="list-content">
<span class="list-headline">Review notes</span>
<span class="list-supporting">Updated this morning</span>
</span>
<span class="list-trailing">PDF</span>
</li>
<!-- Three lines: minimum height 88px; long text can wrap naturally. -->
<li class="list-item list-three-line">
<span class="list-leading">
<i class="material-icons" aria-hidden="true">movie</i>
</span>
<span class="list-content">
<span class="list-headline">Sequence delivery</span>
<span class="list-supporting">
Final renders and review notes.
<br />
Ready for the next production review.
</span>
</span>
<span class="list-trailing">Today</span>
</li>
</ul>Avatars and images
Leading slots support initials, avatars, icons, or thumbnails. Use empty image alt text when the adjacent headline already supplies its meaning.
- Alex LeeLighting artist
-
YunaProduction coordinator
-
Environment referenceLocation and lighting inspiration
<ul class="list" aria-label="Project contacts and references">
<li class="list-item list-two-line">
<span class="list-avatar" aria-hidden="true">AL</span>
<span class="list-content">
<span class="list-headline">Alex Lee</span>
<span class="list-supporting">Lighting artist</span>
</span>
</li>
<li class="list-item list-two-line">
<img class="list-avatar" src="/images/yuna.jpg" alt="" />
<span class="list-content">
<span class="list-headline">Yuna</span>
<span class="list-supporting">Production coordinator</span>
</span>
</li>
<li class="list-item list-three-line">
<img class="list-image" src="/images/sample-1.jpg" alt="" />
<span class="list-content">
<span class="list-headline">Environment reference</span>
<span class="list-supporting">Location and lighting inspiration</span>
</span>
</li>
</ul>Selection and segmented lists
Segmented rows share rounded outer corners. Standard Materialize checkbox inputs support multiple choices, and labels make the entire row clickable. Selection changes the fill while preserving the shape.
<ul class="list list-segmented" aria-label="Delivery formats">
<li>
<label class="list-item list-interactive">
<span class="list-content"><span class="list-headline">Review movie</span></span>
<input
class="list-control"
type="checkbox"
name="delivery"
value="movie"
checked
/>
</label>
</li>
<li>
<label class="list-item list-interactive">
<span class="list-content">
<span class="list-headline">Image sequence</span>
</span>
<input class="list-control" type="checkbox" name="delivery" value="sequence" />
</label>
</li>
<li>
<label class="list-item list-interactive">
<span class="list-content"><span class="list-headline">Review notes</span></span>
<input class="list-control" type="checkbox" name="delivery" value="notes" />
</label>
</li>
</ul>Single choice
Uses the same Materialize radio styling as the Radio Buttons page. Keep a span directly after each input and give the inputs the same name for exclusive selection and arrow-key navigation.
<!-- Keep the enclosing surface padding at zero for edge-to-edge rows. -->
<ul class="list list-flush" aria-label="Review priority">
<li>
<label class="list-item list-interactive">
<input
class="list-control"
type="radio"
name="review-priority"
value="normal"
checked
/>
<span class="list-content"><span class="list-headline">Normal</span></span>
</label>
</li>
<li>
<label class="list-item list-interactive">
<input class="list-control" type="radio" name="review-priority" value="urgent" />
<span class="list-content"><span class="list-headline">Urgent</span></span>
</label>
</li>
</ul>Submission checklist with optional progress
Combine checkboxes, review notes, completion stamps, and a bottom progress footer. Toggle items to update the count and progress. Demo author/time values are client-side display data; your application handles storage and trusted attribution.
<div id="submission-checklist" data-checker="You">
<ul class="list list-segmented" aria-label="Submission checks">
<li>
<label class="list-item list-three-line list-interactive">
<input
class="list-control"
type="checkbox"
name="submission"
value="frames"
checked
/>
<span class="list-content">
<span class="list-headline">Check frame range</span>
<span class="list-supporting list-check-note">
Confirm the first and last frames match the delivery request.
</span>
<!-- Stored attribution can be rendered here before initialization. -->
<span class="list-check-stamp" data-list-stamp>
<i class="material-icons" aria-hidden="true">check_circle</i>
<span>
Checked by
<strong data-list-checker>Alex</strong>
<time datetime="2026-09-10T09:00:00Z">Sep 10, 2026</time>
</span>
</span>
</span>
</label>
</li>
<li>
<label class="list-item list-three-line list-interactive">
<input class="list-control" type="checkbox" name="submission" value="color" />
<span class="list-content">
<span class="list-headline">Verify color space</span>
<span class="list-supporting list-check-note">
Review the display transform before exporting.
</span>
<!-- Stored attribution can be rendered here before initialization. -->
<span class="list-check-stamp" data-list-stamp hidden>
<i class="material-icons" aria-hidden="true">check_circle</i>
<span>
Checked by
<strong data-list-checker></strong>
<time></time>
</span>
</span>
</span>
</label>
</li>
<li>
<label class="list-item list-three-line list-interactive">
<input class="list-control" type="checkbox" name="submission" value="notes" />
<span class="list-content">
<span class="list-headline">Attach review notes</span>
<span class="list-supporting list-check-note">
Include the latest supervisor feedback.
</span>
<!-- Stored attribution can be rendered here before initialization. -->
<span class="list-check-stamp" data-list-stamp hidden>
<i class="material-icons" aria-hidden="true">check_circle</i>
<span>
Checked by
<strong data-list-checker></strong>
<time></time>
</span>
</span>
</span>
</label>
</li>
</ul>
<!-- Optional: remove this entire footer to hide count and progress. -->
<div class="list-checklist-footer">
<span data-list-progress-label role="status">1 / 3 completed</span>
<progress value="1" max="3" aria-label="Submission checklist completion"></progress>
</div>
</div>import { initListChecklist } from 'kmaterialize';
// Initialize the wrapper around the list and its optional footer.
const checklist = document.querySelector<HTMLElement>('#submission-checklist')!;
const destroy = initListChecklist(checklist);
// Save changes using your application's own persistence layer.
checklist.addEventListener('listchange', (event) => {
const { value, checked, completed, total } = (event as CustomEvent).detail;
console.log(value, checked, completed, total);
});
// When removing this view:
// destroy();Checklist without progress
The footer is opt-in markup. This example has no count or progress bar; native checkbox selection works without JavaScript.
<ul class="list list-segmented" aria-label="Quick checks">
<li>
<label class="list-item list-interactive">
<input class="list-control" type="checkbox" value="preview" />
<span class="list-content">
<span class="list-headline">Preview the export</span>
</span>
</label>
</li>
<li>
<label class="list-item list-interactive">
<input class="list-control" type="checkbox" value="filename" />
<span class="list-content">
<span class="list-headline">Check the filename</span>
</span>
</label>
</li>
</ul>Setup and class reference
// Load after Materialize.
// List styles are included in Materialize CSS.| Class | Purpose |
|---|---|
list-flush | Edge-to-edge rows without outer list padding; inherits the container corner radius. |
list / list-item | List container and flex row. |
list-two-line / list-three-line | Minimum row heights of 72px / 88px; base rows are 56px. |
list-content / list-headline / list-supporting | Text hierarchy and wrapping. |
list-leading / list-trailing | Optional visual, metadata, or action slots. |
list-avatar / list-image | Initials, portraits, and thumbnails. |
list-interactive / list-control | Native interactive rows and selection inputs. |
list-segmented | Separated rows with rounded outside corners; --list-gap controls spacing. |
list-divider | Decorative inset separator. |
list-check-stamp / list-check-note | Attribution and review notes. |
list-checklist-footer | Optional bottom count and progress bar. |
Use ul/li for list semantics and native labels for checkbox or radio rows. Mark decorative icons as hidden from assistive technology. Keep controls independently focusable when adding a separate trailing action to a static row.
initListChecklist(root) returns a cleanup function. It reads checked inputs, preserves existing stamp data, and updates any optional data-list-progress-label and progress elements. Set data-checker on the wrapper for the display name. Each change emits listchange with { value: string, checked: boolean, completed: number, total: number }. Disabled items are included in the total. Reinitialize after replacing the list markup.
Component SCSS
// Material-themed lists. Load after Materialize; no changes to legacy collections.
.list {
--list-gap: 2px;
margin: 0;
padding: 8px 0;
list-style: none;
background: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
> li {
list-style: none;
}
}
// Edge-to-edge interactive rows, clipped to the enclosing surface's corners.
.list.list-flush {
padding: 0;
border-radius: inherit;
overflow: hidden;
}
.list-item {
position: relative;
display: flex;
align-items: center;
gap: 16px;
min-height: 56px;
width: 100%;
padding: 8px 16px;
box-sizing: border-box;
color: inherit;
&.list-two-line {
min-height: 72px;
}
&.list-three-line {
min-height: 88px;
align-items: flex-start;
padding-block: 12px;
}
}
.list-content {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
}
.list-headline {
display: block;
font-size: 16px;
line-height: 24px;
color: var(--md-sys-color-on-surface);
}
.list-supporting {
display: block;
font-size: 14px;
line-height: 20px;
color: var(--md-sys-color-on-surface-variant);
}
.list-leading,
.list-trailing {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--md-sys-color-on-surface-variant);
}
.list-trailing {
gap: 8px;
font-size: 12px;
line-height: 16px;
}
.list-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
background: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
.list-image {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 8px;
}
// Use native links, buttons, and labels for their built-in keyboard behavior.
.list-interactive {
border: 0;
background: transparent;
font: inherit;
text-align: start;
cursor: pointer;
&:hover {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}
&:focus-visible {
outline: 2px solid var(--md-sys-color-primary);
outline-offset: -2px;
}
&:disabled {
opacity: 0.38;
cursor: default;
}
}
.list-item:has(.list-control:checked),
.list-item[aria-current='page'] {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
// Checkbox inputs use Materialize's shared checkbox styles without overrides.
// Keep the standard radio visual while allowing list text to wrap.
.list .list-control[type='radio'] + .list-content {
height: auto;
min-height: 25px;
}
.list-item:has(.list-control[type='radio']:focus-visible) {
outline: 2px solid var(--md-sys-color-primary);
outline-offset: -2px;
}
// Segmented rows retain their silhouette when selected.
.list-segmented {
display: flex;
flex-direction: column;
gap: var(--list-gap);
padding: 0;
background: transparent;
> li > .list-item {
border-radius: 4px;
background-color: var(--md-sys-color-surface-variant);
}
> li:first-child > .list-item {
border-start-start-radius: 16px;
border-start-end-radius: 16px;
}
> li:last-child > .list-item {
border-end-start-radius: 16px;
border-end-end-radius: 16px;
}
> li > .list-item:has(.list-control:checked),
> li > .list-item[aria-current='page'] {
background-color: var(--md-sys-color-secondary-container);
}
> li > .list-interactive:hover {
background-color: color-mix(
in srgb,
var(--md-sys-color-surface-variant),
var(--md-sys-color-on-surface) 8%
);
}
}
.list-divider {
height: 1px;
margin: 0 16px 0 56px;
background: var(--md-sys-color-outline-variant);
}
.list-check-stamp {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 12px;
color: var(--md-sys-color-on-surface-variant);
}
.list-check-stamp[hidden] {
display: none;
}
.list-check-stamp > .material-icons {
font-size: 16px;
}
.list-check-note {
margin: 8px 0 0;
padding-inline-start: 12px;
border-inline-start: 2px solid var(--md-sys-color-outline);
font-style: italic;
}
.list-checklist-footer {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
color: var(--md-sys-color-on-surface-variant);
}
.list-checklist-footer > progress {
flex: 1;
min-width: 0;
height: 6px;
accent-color: var(--md-sys-color-primary);
}Checklist TypeScript
/** Enhance a submission-style checklist. Ordinary lists need no initialization. */
export function initListChecklist(root: HTMLElement): () => void {
const events = new AbortController();
const controls = () =>
Array.from(
root.querySelectorAll<HTMLInputElement>('.list-control[type="checkbox"]'),
);
function updateProgress() {
const items = controls();
const completed = items.filter((item) => item.checked).length;
const label = root.querySelector<HTMLElement>('[data-list-progress-label]');
const progress = root.querySelector<HTMLProgressElement>('progress');
if (label) label.textContent = `${completed} / ${items.length} completed`;
if (progress) {
progress.max = Math.max(items.length, 1);
progress.value = completed;
}
return { completed, total: items.length };
}
root.addEventListener(
'change',
(event) => {
const control = event.target;
if (
!(control instanceof HTMLInputElement) ||
!control.matches('.list-control[type="checkbox"]')
)
return;
const stamp = control
.closest('.list-item')
?.querySelector<HTMLElement>('[data-list-stamp]');
if (stamp) {
stamp.hidden = !control.checked;
const name = stamp.querySelector('[data-list-checker]');
const time = stamp.querySelector('time');
if (control.checked) {
if (name) name.textContent = root.dataset.checker || 'You';
if (time) {
const now = new Date();
time.dateTime = now.toISOString();
time.textContent = now.toLocaleString(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
});
}
} else {
if (name) name.textContent = '';
if (time) {
time.textContent = '';
time.removeAttribute('datetime');
}
}
}
root.dispatchEvent(
new CustomEvent('listchange', {
bubbles: true,
detail: {
value: control.value,
checked: control.checked,
...updateProgress(),
},
}),
);
},
{ signal: events.signal },
);
// Preserve supplied author/time data for items checked before initialization.
controls().forEach((control) => {
const stamp = control
.closest('.list-item')
?.querySelector<HTMLElement>('[data-list-stamp]');
if (stamp) stamp.hidden = !control.checked;
});
updateProgress();
return () => events.abort();
}