Vertical list container with items and sections.
Introduction
Lists present a continuous, vertical index of text or images. They are composed of related parts:
- List — wrapper for rows; renders as ul, ol, or nav via the as prop.
- ListItem — a single row with primary and optional secondary text, plus start and end slots.
- ListSection — a subheader that groups related items.
List vs Menu content: A List is part of the document flow—side navigation, settings screens, or static indexes. A Menu is a temporary floating surface anchored to a trigger; its panel often contains a List with className="p-0" and ListItem rows using role="menuitem". Use List for persistent layout; wrap it in Menu when choices should appear on demand and dismiss after selection.
Import
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { ListSection } from "@bridge-ui/vue/Components/ListSection";import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { ListSection } from "@bridge-ui/react/Components/ListSection";Basic usage
Wrap ListItem rows inside a List. Use primary and secondary for label text.
<script setup lang="ts">
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
</script>
<template>
<div class="w-full">
<List>
<ListItem primary="Inbox" secondary="12 messages" />
<ListItem primary="Drafts" secondary="3 items" />
<ListItem primary="Sent" />
</List>
</div>
</template>
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
export default function ListBasic() {
return (
<div className="w-full">
<List>
<ListItem primary="Inbox" secondary="12 messages" />
<ListItem primary="Drafts" secondary="3 items" />
<ListItem primary="Sent" />
</List>
</div>
);
}
Sections
Use ListSection to add subheaders that group related items. Add leading and trailing content to items with the start and end slots.
With the default as="li", sticky applies on the section root (with an opaque background) so the heading can stick while sibling items scroll. Use as="div" when you need sticky on the title element itself. The list (or a parent) needs a scroll container with a constrained height.
<script setup lang="ts">
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { ListSection } from "@bridge-ui/vue/Components/ListSection";
import { Inbox, Star, Tag } from "@lucide/vue";
</script>
<template>
<div class="w-full">
<List>
<ListSection title="Folders" />
<ListItem primary="Inbox" secondary="12 unread messages">
<template #start>
<Inbox class="text-secondary-500 size-5" />
</template>
<template #end>
<span class="text-secondary-400 text-xs">12</span>
</template>
</ListItem>
<ListItem primary="Starred">
<template #start>
<Star class="text-secondary-500 size-5" />
</template>
</ListItem>
<ListSection title="Labels" />
<ListItem primary="Important">
<template #start>
<Tag class="text-secondary-500 size-5" />
</template>
</ListItem>
</List>
</div>
</template>
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { ListSection } from "@bridge-ui/react/Components/ListSection";
import { Inbox, Star, Tag } from "lucide-react";
export default function ListSections() {
return (
<div className="w-full">
<List>
<ListSection title="Folders" />
<ListItem
primary="Inbox"
secondary="12 unread messages"
slots={{
start: <Inbox className="text-secondary-500 size-5" />,
end: <span className="text-secondary-400 text-xs">12</span>,
}}
/>
<ListItem
primary="Starred"
slots={{
start: <Star className="text-secondary-500 size-5" />,
}}
/>
<ListSection title="Labels" />
<ListItem
primary="Important"
slots={{
start: <Tag className="text-secondary-500 size-5" />,
}}
/>
</List>
</div>
);
}
Interactive items
Set interactive on ListItem to apply hover and focus styles. Use selected to highlight the active row and customProps.interactive (React) or custom-props (Vue) to handle clicks.
<script setup lang="ts">
import { ref } from "vue";
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
const selected = ref("profile");
</script>
<template>
<div class="w-full">
<List role="menu">
<ListItem
interactive
role="menuitem"
primary="Profile"
:selected="selected === 'profile'"
:custom-props="{
interactive: { onClick: () => (selected = 'profile') },
}"
/>
<ListItem
interactive
role="menuitem"
primary="Settings"
:selected="selected === 'settings'"
:custom-props="{
interactive: { onClick: () => (selected = 'settings') },
}"
/>
<ListItem
interactive
role="menuitem"
primary="Sign out"
:selected="selected === 'signout'"
:custom-props="{
interactive: { onClick: () => (selected = 'signout') },
}"
/>
</List>
</div>
</template>
import { useState } from "react";
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
export default function ListInteractive() {
const [selected, setSelected] = useState("profile");
return (
<div className="w-full">
<List role="menu">
<ListItem
interactive
role="menuitem"
primary="Profile"
selected={selected === "profile"}
customProps={{
interactive: { onClick: () => setSelected("profile") },
}}
/>
<ListItem
interactive
role="menuitem"
primary="Settings"
selected={selected === "settings"}
customProps={{
interactive: { onClick: () => setSelected("settings") },
}}
/>
<ListItem
interactive
role="menuitem"
primary="Sign out"
selected={selected === "signout"}
customProps={{
interactive: { onClick: () => setSelected("signout") },
}}
/>
</List>
</div>
);
}
Selected icon
Selected rows show a check icon by default. Customize it with selectedIcon on ListItem, or pass null to hide it. Providing slots.end replaces the selected icon.
<script setup lang="ts">
import { Star } from "@lucide/vue";
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
</script>
<template>
<div class="w-full">
<List>
<ListItem selected interactive primary="Inbox" />
<ListItem selected interactive primary="Starred" :selected-icon="Star" />
<ListItem selected interactive primary="No icon" :selected-icon="null" />
</List>
</div>
</template>
import { Star } from "lucide-react";
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
export default function ListSelectedIcon() {
return (
<div className="w-full">
<List>
<ListItem selected interactive primary="Inbox" />
<ListItem selected interactive primary="Starred" selectedIcon={Star} />
<ListItem selected interactive primary="No icon" selectedIcon={null} />
</List>
</div>
);
}
Dense
Pass dense to the List to reduce vertical spacing on child items and sections.
<script setup lang="ts">
import { List } from "@bridge-ui/vue/Components/List";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { ListSection } from "@bridge-ui/vue/Components/ListSection";
</script>
<template>
<div class="grid w-full gap-6 sm:grid-cols-2">
<div>
<p class="text-secondary-500 mb-2 text-sm font-medium">Normal</p>
<List>
<ListSection title="Account" />
<ListItem primary="Profile" secondary="Manage your account" />
<ListItem primary="Notifications" secondary="Email and push" />
</List>
</div>
<div>
<p class="text-secondary-500 mb-2 text-sm font-medium">Dense</p>
<List dense>
<ListSection title="Account" />
<ListItem primary="Profile" secondary="Manage your account" />
<ListItem primary="Notifications" secondary="Email and push" />
</List>
</div>
</div>
</template>
import { List } from "@bridge-ui/react/Components/List";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { ListSection } from "@bridge-ui/react/Components/ListSection";
export default function ListDense() {
return (
<div className="grid w-full gap-6 sm:grid-cols-2">
<div>
<p className="text-secondary-500 mb-2 text-sm font-medium">Normal</p>
<List>
<ListSection title="Account" />
<ListItem primary="Profile" secondary="Manage your account" />
<ListItem primary="Notifications" secondary="Email and push" />
</List>
</div>
<div>
<p className="text-secondary-500 mb-2 text-sm font-medium">Dense</p>
<List dense>
<ListSection title="Account" />
<ListItem primary="Profile" secondary="Manage your account" />
<ListItem primary="Notifications" secondary="Email and push" />
</List>
</div>
</div>
);
}
Related components
Accessibility
- Set as="nav" on List when it represents site or section navigation.
- On interactive rows, set interactive so hover and focus styles apply and tabIndex={0} is added.
- When items appear inside a Menu, set role="menuitem" on ListItem (or "option" inside a listbox pattern).
- Use selected to highlight the active item; pair with visible focus styles for keyboard users.
- Set disabled on non-actionable rows so they are skipped or muted appropriately.
Anatomy
<ul> <!-- List root -->
<li> <!-- ListSection -->
<div> <!-- section title -->
</li>
<li> <!-- ListItem -->
<div> <!-- start slot -->
<div> <!-- content (primary + secondary) -->
<div> <!-- end slot -->
</li>
</ul>Use classes on each component to style list, item, and section parts independently.
API
List
| Prop | Type | Default | Description |
|---|---|---|---|
| as | "nav" | "ol" | "ul" | "ul" | The element to render as. |
| dense | boolean | false | Compact vertical spacing on child items and sections. |
| nested | boolean | false | Indents the list for nested navigation. |
| classes | ListClasses | — | Classes for the list root. |
| customProps | ListCustomProps | — | Props forwarded to list parts. |
ListItem
| Prop | Type | Default | Description |
|---|---|---|---|
| primary | ReactNode | — | Primary label text. |
| secondary | ReactNode | — | Secondary text below the primary line. |
| interactive | boolean | false | Applies hover/focus styles and tabIndex={0}. |
| selected | boolean | false | Highlights the item as selected. |
| selectedIcon | null | IconSource | "check" | Icon shown when selected is true. Use null to hide it. Replaced by slots.end. |
| value | ListboxValue | — | When set inside a Select / Listbox, registers this row as a selectable option. |
| disabled | boolean | false | Mutes the item and disables interaction. |
| divider | boolean | false | Renders a bottom divider. |
| dense | boolean | — | Compact padding. Inherits from parent List when omitted. |
| as | "div" | "li" | "li" | The element to render as. |
| role | "button" | "menuitem" | "option" | "button" | ARIA role for the interactive wrapper. |
| classes | ListItemClasses | — | Classes for item parts. |
| customProps | ListItemCustomProps | — | Props forwarded to item parts, including interactive for click handlers. |
| slots | ListItemSlots | — | React slots: start, end, primary, secondary. Vue slots: #start, #end. |
ListSection
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | Section label text. |
| inset | boolean | false | Adds left padding to align with items that have leading icons. |
| sticky | boolean | false | Sticks the heading while scrolling. On as="li" (default), sticky + opaque background apply to the root; on as="div", to the title. |
| as | "div" | "li" | "li" | The element to render as. |
| classes | ListSectionClasses | — | Classes for section parts. |
| customProps | ListSectionCustomProps | — | Props forwarded to section parts. |