Selectable list with left badge, label, and right-aligned count/icon. Matches the sidebar
pattern. Use the arrow keys, Home and End to move between items.
<script lang="ts">
import { CwListBox } from '$lib/index.js';
import type { CwListBoxItem } from '$lib/index.js';
const groups: CwListBoxItem<string>[] = [
{ value: 'all', label: 'All groups', badge: 'ALL', badgeTone: 'secondary', endText: '196' },
{ value: 'ungrouped', label: 'Ungrouped', badge: 'UNG', badgeTone: 'secondary', endText: '91' },
{ value: 'seagaia', label: 'Seagaia', badge: 'SEA', badgeTone: 'success', endText: '41' },
{ value: 'sa', label: 'SA', badge: 'SA', badgeTone: 'info', endText: '11' },
{ value: 'kokokara', label: 'Kokokara', badge: 'KOK', badgeTone: 'warning', endText: '2' },
{ value: 'toyota', label: 'Toyota', badge: 'TOY', badgeTone: 'danger', endText: '2' },
{ value: 'tk-ebisu', label: 'TK-Ebisu', badge: 'TK-', badgeTone: 'primary', endText: '49' }
];
const locations: CwListBoxItem<string>[] = [
{ value: 'all-loc', label: 'All locations', endText: '196' },
{ value: 'miyazaki', label: 'Miyazaki House', endText: '7' },
{ value: 'npo', label: 'NPO法人 東米良創生会', endText: '1' },
{ value: 'pine1', label: 'パインテラス 1 階', endText: '26' },
{ value: 'pine-b1', label: 'パインテラス地下 1 階', endText: '6' },
{ value: 'sa-farm', label: 'SA西都農場', endText: '3' },
{ value: 'kawagoe', label: '川越農産', endText: '3' },
{ value: 'takama', label: '高間ハウス', endText: '3' },
{ value: 'toyotama', label: 'とよたま 2 階', endText: '9' }
];
const navItems: CwListBoxItem<string>[] = [
{ value: 'home', label: 'Home', endText: '↗' },
{ value: 'locations', label: 'Locations', endText: '↗' },
{ value: 'rules', label: 'Rules', endText: '↗' },
{ value: 'reports', label: 'Reports', endText: '↗' },
{ value: 'gateways', label: 'Gateways', endText: '↗' }
];
let selectedGroup = $state<string | null>('all');
let selectedLocation = $state<string | null>('all-loc');
let selectedNav = $state<string | null>('home');
</script>
<div class="demo-columns">
<div class="demo-sidebar">
<CwListBox
heading="Groups"
items={groups}
bind:value={selectedGroup}
/>
<CwListBox
heading="Locations"
items={locations}
bind:value={selectedLocation}
/>
</div>
<div class="demo-sidebar">
<CwListBox
heading="Navigation"
items={navItems}
bind:value={selectedNav}
/>
<div class="demo-sidebar__footer">
<p>Selected group: <strong>{selectedGroup}</strong></p>
<p>Selected location: <strong>{selectedLocation}</strong></p>
</div>
</div>
</div>
API
Usage and API reference
CwListBox is an always-visible, single-select list with optional badges and trailing text. It is a proper ARIA listbox: one tab stop, arrow keys to move, Enter or Space to select.
How to think about it
- Name the list A visible `heading` also labels the listbox. Without one, pass `ariaLabel` so screen readers announce what the options are.
- Bind the value Use `bind:value` for the selected item value, or listen with `onselect` to receive the whole item.
- Plan for empty data Set `emptyText` so an empty list explains itself instead of rendering nothing.
Props and callbacks
| API | Type | Details |
|---|
items Required | CwListBoxItem<T>[] | `{ value, label, badge?, badgeTone?, endText?, disabled? }` entries. |
value Default: null | T | null | Bindable selected value. |
heading | string | Visible heading that also labels the list. |
ariaLabel Default: 'Options' | string | Accessible name when there is no heading. |
emptyText | string | Message shown when `items` is empty. |
onselect | (item: CwListBoxItem<T>) => void | Called when an item is selected. |
itemIcon / itemEnd | Snippet<[CwListBoxItem<T>]> | Custom content on the left or right of each row. |
Copy-paste examples
Each snippet is a complete, working example you can paste into a Svelte 5 component.
Region picker
<script lang="ts">
import { CwListBox } from '@cropwatchdevelopment/cwui';
let region = $state<string | null>('as923');
</script>
<CwListBox
heading="Frequency plan"
bind:value={region}
items={[
{ value: 'as923', label: 'Asia 923 MHz', badge: 'AS', endText: '12 gateways' },
{ value: 'eu868', label: 'Europe 868 MHz', badge: 'EU', endText: '4 gateways' }
]}
emptyText="No regions configured"
/>