Skip to main content

CollapsibleList

A categorised sidebar navigation list with collapsible sections, dot-indicator active state, and optional icons. Inspired by the mates-docs learn sidebar.

Quick start
import { CollapsibleList } from "mates-ui";

const active = atom("intro");

CollapsibleList({
  title: "Docs",
  categories: [
    {
      id: "getting-started",
      title: "Getting started",
      items: [
        { id: "intro",      label: "Introduction" },
        { id: "install",    label: "Installation" },
        { id: "quickstart", label: "Quick start"  },
      ],
    },
  ],
  activeId: active(),
  onSelect: (item) => active.set(item.id),
})

Basic — docs sidebar

import { html, atom, renderApp } from 'mates';
import { CollapsibleList, Text } from 'mates-ui';
import { BoltIcon, DataObjectIcon, HomeIcon, HubIcon, LayersIcon, RocketLaunchIcon } from 'mates-icons';

const CATEGORIES = [
{
id: "getting-started", title: "Getting started",
items: [
{ id: "intro", label: "Introduction", icon: HomeIcon({ size: 16 }) },
{ id: "install", label: "Installation", icon: RocketLaunchIcon({ size: 16 }) },
{ id: "quickstart", label: "Quick start", icon: BoltIcon({ size: 16 }) },
],
},
{
id: "core", title: "Core concepts",
items: [
{ id: "atoms", label: "Atoms & state", icon: DataObjectIcon({ size: 16 }) },
{ id: "components", label: "Components", icon: LayersIcon({ size: 16 }) },
{ id: "routing", label: "Routing", icon: HubIcon({ size: 16 }) },
],
},
];

const App = () => {
const active = atom("intro");
return () => html`
<x-row gap="var(--md-space-5)" align="flex-start">
<div style="width:14rem;border:0.06rem solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${CollapsibleList({ title: "Docs", categories: CATEGORIES, activeId: active(), onSelect: (item) => active.set(item.id) })}
</div>
<x-col gap="var(--md-space-1)">
${Text("Selected", { type: "label-sm", color: "muted" })}
${Text(active(), { type: "body-md" })}
</x-col>
</x-row>
`;

Dot-only — lessons sidebar (no icons)

import { html, atom, renderApp } from 'mates';
import { CollapsibleList, Text } from 'mates-ui';

const CATEGORIES = [
{
id: "basics", title: "Basics",
items: [
{ id: "l1", label: "Hello world" },
{ id: "l2", label: "Reactive atoms" },
{ id: "l3", label: "Templates" },
{ id: "l4", label: "Events" },
],
},
{
id: "routing", title: "Routing",
items: [
{ id: "l8", label: "Client-side routing" },
{ id: "l9", label: "Nested routes" },
{ id: "l10", label: "Guards & redirects", disabled: true },
],
},
];

const App = () => {
const active = atom("l2");
return () => html`
<x-row gap="var(--md-space-5)" align="flex-start">
<div style="width:14rem;border:0.06rem solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${CollapsibleList({ title: "Lessons", categories: CATEGORIES, activeId: active(), onSelect: (item) => active.set(item.id) })}
</div>
${Text("Selected: " + active(), { type: "body-sm", color: "muted" })}
</x-row>
`;
};

renderApp(App, document.getElementById('app'));

Icons only — no dot (showDot: false)

import { html, atom, renderApp } from 'mates';
import { CollapsibleList } from 'mates-ui';
import { NotificationsIcon, PersonIcon, SettingsIcon } from 'mates-icons';

const CATEGORIES = [
{
id: "account", title: "Account",
items: [
{ id: "profile", label: "Profile", icon: PersonIcon({ size: 16 }) },
{ id: "notifications", label: "Notifications", icon: NotificationsIcon({ size: 16 }) },
],
},
{
id: "app", title: "App settings",
items: [
{ id: "general", label: "General", icon: SettingsIcon({ size: 16 }) },
{ id: "advanced", label: "Advanced", icon: SettingsIcon({ size: 16 }) },
],
},
];

const App = () => {
const active = atom("profile");
return () => html`
<div style="width:14rem;border:0.06rem solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${CollapsibleList({ title: "Settings", showDot: false, categories: CATEGORIES, activeId: active(), onSelect: (item) => active.set(item.id) })}
</div>
`;
};

renderApp(App, document.getElementById('app'));

Controlled open state

import { html, atom, renderApp } from 'mates';
import { CollapsibleList, OutlinedButton, Text } from 'mates-ui';

const CATEGORIES = [
{
id: "basics", title: "Basics",
items: [
{ id: "l1", label: "Hello world" },
{ id: "l2", label: "Reactive atoms" },
{ id: "l3", label: "Templates" },
],
},
{
id: "routing", title: "Routing",
items: [
{ id: "l8", label: "Client-side routing" },
{ id: "l9", label: "Nested routes" },
],
},
{
id: "advanced", title: "Advanced",
items: [
{ id: "l15", label: "SSR" },
{ id: "l16", label: "Testing" },
],
},
];

const App = () => {
const active = atom("l1");
const openIds = atom(["basics", "routing"]);
return () => html`
<x-row gap="var(--md-space-5)" align="flex-start">
<div style="width:14rem;border:0.06rem solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${CollapsibleList({
title: "Lessons", showCount: true, categories: CATEGORIES,

No header (showCount: false, no title)

import { html, atom, renderApp } from 'mates';
import { CollapsibleList } from 'mates-ui';

const CATEGORIES = [
{
id: "basics", title: "Basics",
items: [
{ id: "l1", label: "Hello world" },
{ id: "l2", label: "Reactive atoms" },
{ id: "l3", label: "Templates" },
],
},
{
id: "routing", title: "Routing",
items: [
{ id: "l8", label: "Client-side routing" },
{ id: "l9", label: "Nested routes" },
],
},
];

const App = () => {
const active = atom("l1");
return () => html`
<div style="width:14rem;border:0.06rem solid var(--md-color-border);border-radius:var(--md-radius-md);overflow:hidden;">
${CollapsibleList({ showCount: false, categories: CATEGORIES, activeId: active(), onSelect: (item) => active.set(item.id) })}
</div>
`;
};

renderApp(App, document.getElementById('app'));

CollapsibleList

import { CollapsibleList } from "mates-ui";
Prop Type Default Description
activeId string ID of the currently selected item.
attr AttrMap HTML attributes on the root element.
categories* CollapsibleListCategory[] Ordered list of category groups, each containing items.
classes string | string[] Extra root classes — joined and appended after built-in classes.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
on OnEventMap DOM event handlers on the root element.
onSelect (item: CollapsibleListItem) => void Called when the user clicks an item.
onToggleCategory (id: string, open: boolean) => void Called when a category is expanded or collapsed.
openIds string[] Controlled list of open category IDs. When omitted all categories start open (uncontrolled).
showCount boolean true Show the total item count in the header.
showDot boolean true Show the dot indicator on each item.
style StyleMap Root inline CSS (`style` prop; `style` wins if both set).
style StyleMap Inline CSS on the root element.
title string Optional header title shown above the list.

CollapsibleListCategory

Prop Type Default Description
id* string Unique identifier for the category.
items* CollapsibleListItem[] Items rendered inside this category.
title* string Label shown in the category header button.

CollapsibleListItem

Prop Type Default Description
data unknown Any extra data the consumer wants to carry on the item.
disabled boolean Prevents the item from being selected.
icon TemplateResult Optional icon rendered before the label.
id* string Unique identifier matched against activeId.
label* string Visible item label.

Caveats

  • All categories are open by default (uncontrolled). Pass openIds to control expansion state.
  • The component renders inline — wrap in a fixed-height container with overflow:auto for sidebar use.