Always-visible option list in a bordered box: the list that stays open. Single or multiple selection, with an optional search box. Use it on a settings page where the user should see every option at once; for a list that should collapse use sui-select. Form-associated: posts the value, one entry per pick when multiple.
import { Component } from "@angular/core";
import { SuiListbox } from "@shineui/angular";
@Component({
standalone: true,
imports: [SuiListbox],
template: `<sui-listbox label="Customer" name="customer" value="Riverbend Dental Group" [options]="options" (suiChange)="onChange($event)"></sui-listbox>`,
})
export class Example {
options = ["Clark County Schools", "Derby City Events", "Riverbend Dental Group", "Silver Hills Church"]; // SuiListboxEntry[]
onChange(e: CustomEvent) { console.log(e.detail); }
}
Basic
The list itself is the tab stop: a role="listbox" with aria-activedescendant, so the active row moves without moving focus. Disabled options carry aria-disabled and the arrows step over them.
multiple turns value into an array and every row keeps its own aria-selected. The search box narrows the rows without changing the selection.
<div style="max-width:340px">
<sui-listbox id="lb-multi" label="Customers to email" multiple filter height="190" empty-message="No customer matches that name."></sui-listbox>
</div>
// setup(root): properties and events
setup(root) {
const el = root.querySelector('#lb-multi');
el.options = customers;
el.value = ['Derby City Events', 'Silver Hills Church'];
}
Grouped options
A group is { label, items } in the options array and draws an eyebrow header over its rows. The rows stay one list, so the arrows walk straight through a header.
While loading is set the box shows the loading row instead of options and carries aria-busy. Set it before the fetch and clear it when the options land.
An error replaces the help line, paints the box in the danger tone and sets aria-invalid. invalid is the same tone with no message, for a form that draws its summary elsewhere.
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:start">
<sui-listbox id="lb-help" label="Report" help="The report runs against the selected period." height="150"></sui-listbox>
<sui-listbox id="lb-err" label="Report" required error="Choose a report to run." height="150"></sui-listbox>
<sui-listbox id="lb-inv" label="Report" required invalid help="The report runs against the selected period." height="150"></sui-listbox>
<sui-listbox id="lb-off" label="Report" disabled height="150"></sui-listbox>
<sui-listbox id="lb-empty" label="Report" height="150" empty-message="No report is available for this period."></sui-listbox>
</div>
From virtualScrollThreshold options, 200 by default, the box draws only the rows near the view and stands the rest in a blank the height of the list. The arrows, Home, End, typeahead and the position a screen reader reads still count over every option.
emptyMessage is the row for a list that has nothing in it and emptyFilterMessage the row for a search that found nothing; they are two states and two sentences, so "Nothing here yet." is never shown over a long list the reader has just narrowed to none.
suiChange fires on every pick, with the value in single mode and the full array when multiple. The list has no open state, so it emits no suiOpen or suiClose.
Moves the active row, skipping disabled options and wrapping at either end.
HomeEnd
Moves to the first or last enabled row. In the search box the caret keeps them.
Enter
Picks the active row; with multiple, toggles it.
Space
Picks the active row; with multiple, toggles it. In the search box it types a space.
Printable characters
Jumps the active row to the next label starting with the typed text. In the search box they filter the rows.
API
<sui-listbox>
Properties
Property
Attribute
Type
Default
Description
disabled
disabled
boolean
false
Disables the list. A disabled list posts nothing and never holds the form.
emptyFilterMessage
empty-filter-message
stringstring | undefined
Row shown when the filter matched nothing. An empty list and a search that found nothing are different states and one sentence cannot be right about both: "Nothing here yet." is wrong over a list of two hundred the reader has just narrowed to none.
emptyMessage
empty-message
stringstring | undefined
Row shown when the option list is empty.
error
error
stringstring | undefined
Error message; paints the box in the danger tone and sets aria-invalid.
filter
filter
boolean
false
Search box at the top of the list. It narrows the rows without changing the selection.
height
height
number
220
Maximum list height in px; the box scrolls beyond it and keeps the active row in view.
help
help
stringstring | undefined
Help text under the list. Replaced by error when set.
invalid
invalid
boolean
false
Danger border without a message, for a form that lists every problem in one summary block. It is the tone only: it reports no validity error of its own, and the caller clears it.
label
label
stringstring | undefined
Label above the list.
loading
loading
boolean
false
Shows the options are still on their way: the box shows the loading row instead of options. Set it while fetching, clear it when options land.
loadingMessage
loading-message
stringstring | undefined
Text of the loading row.
multiple
multiple
boolean
false
Allows more than one selection; value and the suiChange detail become an array.
name
name
stringstring | undefined
Form control name. Reflected: the form entry is built from the content attribute.
options
property only
SuiListboxEntry[]
[]
Options: strings or numbers, { label, value, disabled } objects, or { label, items } groups whose items are either. A group draws a header over its rows. Property, not attribute.
readOnly
readonly
boolean
false
Display only: the rows can be read, walked and searched but not picked, and the picks still post. Neither a read-only nor a disabled list holds the form on valueMissing.
required
required
boolean
false
Marks the control required for form validation (at least one selection).
value
value
SuiChoiceValueSuiKey[] | number | string | undefined
The picked option's value, or the array of picked values when multiple, each as its option gives it (a number stays a number). Written as an attribute it is text, and it still picks the option whose value reads the same. Mutable; written before suiChange fires.
virtualScrollThreshold
virtual-scroll-threshold
number
200
Option count at which the box starts mounting only the rows near the viewport instead of all of them, so a list of exactly this many is windowed. The keyboard, the ids and the counts a screen reader reads still cover every option. Set it to 0 to keep every row in the DOM whatever the list costs, or to 1 to window a short list under test.
Events
Event
Detail
React
Vue
Description
suiChange
SuiChoiceValueSuiKey[] | number | string
onSuiChange
@sui-change
Fires when the selection changes; detail is the new value: the picked option's value, or the array of picked values when multiple.
Methods
Method
Signature
Description
setFocus()
setFocus() => Promise<void>
Moves keyboard focus to the list (or the filter box when present), as el.focus() does.
Types
SuiChoiceValuetypeWhat a choice element holds: one option's value, or, where it takes several (multiple), the array of the picked values. A value keeps the type its option gave it, so a numeric id comes back a number through value, the events and a framework binding; only the form posts text.
No members.
SuiKeytypeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiListboxEntrytypeOne entry of options: a string or a number, an option object, or a group.
No members.
SuiListboxGroupinterfaceA group: its header label and the options under it. Groups never nest.
label
string
items
SuiListboxOptionInput[]
SuiListboxOptioninterfaceOne option. value is what value holds, as given: a number stays a number through value, suiChange and a framework binding. The form posts its text.
label
string
value
SuiKey
disabled?
boolean
SuiListboxOptionInputtypeAn option: a string or a number stands for itself as label and value.
No members.
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.