List
A container that displays a collection of related items in a structured format. Lists help organize content clearly and improve readability by grouping similar elements together.
Usage
Use lists to present multiple related items, such as options, tasks, or entries. Avoid using lists for grouping unrelated content.
Behavior
An item is a container rather than a control, so a list is made interactive by putting a real button or link inside the item with x-h-list-item-button. Everything about the interaction (@click, href, aria-current and disabled) belongs on that control.
The control fills the row and takes the row's padding, so the area that responds to a click is the area that lights up. The item is what paints, which is why the highlight still spans the full width.
A button placed beside the control, for an action on the row rather than the row itself, is an ordinary x-h-button and stays outside x-h-list-item-button. It gets its own hover and leaves the row alone, so the two never light up at once.
Which row is the current one stays yours to manage. Bind aria-current on the control to mark it, since the list never changes it. Use page for a list of destinations and a bound boolean for a list you select from.
Supporting content in a row, whether a secondary line of text or a decorative icon, goes on x-h-list-secondary. It renders muted and reacts to a highlighted row, so it stays legible. Use it instead of text-muted-foreground inside a list.
Keyboard Handling
The row control is a native button or link, so it behaves like one and the list adds nothing of its own. Each control is a separate tab stop and the arrow keys are not used. For a group that Tab enters once and the arrow keys move through, use the Listbox component instead.
Tab/Shift+Tab- Moves focus to the next or previous control, including any action button beside a row control.Enter/Space- Activates the focused button. A link activates onEnter.
Accessibility
An item is always a list item, whether the list is interactive or not, so the list is announced as a list and with its item count. This is why the control goes inside the item rather than on it.
An item that holds only an icon needs an aria-label on its control, naming the row rather than the icon.
API Reference
Component attribute(s)
x-h-list
x-h-list-item
x-h-list-item-button
x-h-list-secondary
x-h-list-headerAttributes
x-h-list-item-button
| Attribute | Type | Required | Description |
|---|---|---|---|
| aria-current | string | false | Marks the row as the current one, which highlights it. Use page in a list of destinations, or bind a boolean elsewhere. |
| aria-disabled | boolean | false | Marks the control unavailable while keeping it focusable and announced. On a button, disabled removes it from the tab order instead. |
Data Slots
| Slot | Element |
|---|---|
list | x-h-list |
list-item | x-h-list-item |
list-item-button | x-h-list-item-button |
list-secondary | x-h-list-secondary, unless another component on the same element sets its own slot |
list-header | x-h-list-header |
Examples
<ul x-h-list>
<li x-h-list-item>List Item 1</li>
<li x-h-list-item>List Item 2</li>
<li x-h-list-item>List Item 3</li>
</ul>Interactive with disabled items
<ul x-h-list x-data="{ selected: 3 }">
<template x-for="item in [1, 2, 3, 4, 5]" :key="item">
<li x-h-list-item>
<button x-h-list-item-button :aria-current="selected === item" @click="selected = item" :disabled="item === 4 || item === 2" x-text="'List Item ' + item"></button>
</li>
</template>
</ul>With secondary text and icons
The icon and the second line share the x-h-list-secondary slot, so both are muted and both stay readable on the current row.
<ul x-h-list x-data="{ selected: 2 }">
<template x-for="item in [1, 2, 3]" :key="item">
<li x-h-list-item>
<button x-h-list-item-button class="items-center" :aria-current="selected === item" @click="selected = item">
<svg x-h-lucide x-h-list-secondary class="size-5 shrink-0" role="presentation" data-lucide="file-text"></svg>
<div class="vbox min-w-0 flex-1">
<span x-text="'List Item ' + item"></span>
<span x-h-list-secondary class="text-sm" x-text="'Secondary line for item ' + item"></span>
</div>
</button>
</li>
</template>
</ul>Interactive with a row action
<ul x-h-list x-data="{ selected: 1, saved: null }">
<template x-for="item in [1, 2, 3]" :key="item">
<li x-h-list-item>
<button x-h-list-item-button>
<svg x-h-icon class="size-6" data-link="/harmonia/logo/harmonia-symbolic.svg" role="presentation"></svg>
<span x-text="'List Item ' + item"></span>
</button>
<button x-h-button data-variant="outline" data-size="icon" :aria-label="'Save list item ' + item" @click="saved = item">
<svg x-h-lucide role="presentation" data-lucide="save"></svg>
</button>
</li>
</template>
</ul>As links
<ul x-h-list>
<li x-h-list-item>
<a x-h-list-item-button href="/harmonia/components/list.html" aria-current="page">List</a>
</li>
<li x-h-list-item>
<a x-h-list-item-button href="/harmonia/components/listbox.html">Listbox</a>
</li>
<li x-h-list-item>
<a x-h-list-item-button href="/harmonia/components/combobox.html">Combobox</a>
</li>
</ul>With header
<ul x-h-list>
<li x-h-list-header>Group 1</li>
<li x-h-list-item>List Item 1</li>
<li x-h-list-item>List Item 2</li>
<li x-h-list-item>List Item 3</li>
</ul>With icons and buttons
<ul x-h-list>
<li x-h-list-item>
<svg x-h-icon class="size-6" data-link="/harmonia/logo/harmonia-symbolic.svg" role="presentation"></svg>
List Item 1
<div class="flex-1"></div>
<button x-h-button data-variant="outline" data-size="icon-sm" aria-label="Save button">
<svg x-h-lucide role="presentation" data-lucide="save"></svg>
</button>
</li>
<li x-h-list-item>
<svg x-h-icon class="size-6" data-link="/harmonia/logo/harmonia-symbolic.svg" role="presentation"></svg>
List Item 2
</li>
<li x-h-list-item>
<svg x-h-icon class="size-6" data-link="/harmonia/logo/harmonia-symbolic.svg" role="presentation"></svg>
List Item 3
</li>
</ul>