Selection
Use a select for a known set of values, a combobox to search that set, autocomplete when arbitrary text is valid, and a listbox when options should remain visible. Each example imports from @static-bloom/vue and has its own SbProvider.
The tables list wrapper props and overrides. Roots add only class to their named Reka root interface; the remaining Reka props and declared emits pass through, including v-model with update:modelValue. All parts accept merged class (HTMLAttributes['class'], unset). Styling props belong on the trigger, input or filter, rather than on the root.
SbSelect
Section titled “SbSelect”SbSelect wraps Reka SelectRoot. Its trigger displays a value, and content includes a portal, viewport and scroll buttons. Named values are submitted through hidden inputs; the wrapper suppresses Reka’s native select path for the in-game compositor.
Spawn: Harbor
<script setup lang="ts">import { ref, useId } from 'vue'import { SbLabel, SbProvider, SbSelect, SbSelectTrigger, SbSelectValue, SbSelectContent, SbSelectGroup, SbSelectLabel, SbSelectItem, SbSelectSeparator,} from '@static-bloom/vue'const district = ref('Harbor')const id = useId()</script>
<template> <SbProvider> <div class="w-full max-w-sm space-y-2"> <SbLabel :for="id">Spawn district</SbLabel> <SbSelect v-model="district"> <SbSelectTrigger :id="id" ><SbSelectValue placeholder="Choose a district" /></SbSelectTrigger> <SbSelectContent aria-label="Spawn districts"> <SbSelectGroup> <SbSelectLabel>City</SbSelectLabel> <SbSelectItem value="Harbor">Harbor</SbSelectItem> <SbSelectItem value="Old quarter">Old quarter</SbSelectItem> <SbSelectItem value="Restricted" disabled>Restricted</SbSelectItem> </SbSelectGroup> <SbSelectSeparator /> <SbSelectItem value="North depot">North depot</SbSelectItem> </SbSelectContent> </SbSelect> <p class="text-sm text-fg-muted">Spawn: {{ district }}</p> </div> </SbProvider></template>Anatomy
Section titled “Anatomy”<SbSelect> <SbSelectTrigger><SbSelectValue /></SbSelectTrigger> <SbSelectContent> <SbSelectGroup> <SbSelectLabel /> <SbSelectItem value="Harbor" /> </SbSelectGroup> <SbSelectSeparator /> <SbSelectItem value="North depot" /> </SbSelectContent></SbSelect>SbSelectScrollUpButton and SbSelectScrollDownButton are supplied by content and are also exported.
| Component / prop | Type | Default | Effect |
|---|---|---|---|
Root class |
HTMLAttributes['class'] |
unset | Accepted by the interface; root is renderless, so style its parts |
Trigger size |
'sm' | 'md' | 'lg' |
'md' |
Trigger dimensions |
Trigger invalid |
boolean |
false |
Invalid border and aria-invalid |
Content position |
SelectContentProps['position'] |
'popper' |
'popper' or 'item-aligned' positioning |
Content sideOffset |
number |
4 |
Distance from anchor in popper mode |
Content portalProps |
SelectPortalProps |
unset | Portal configuration, including destination |
The root’s name falls back to its surrounding field’s name. Give SbSelectTrigger a label via an associated label or aria-label; the value’s placeholder is not its accessible name. Disabled and invalid states reach the trigger.
ArrowDown opens the listbox, arrow navigation skips disabled options, and Enter selects. Typeahead finds a matching option. Escape closes and returns focus to the trigger. The trigger exposes aria-expanded, and selected options expose aria-selected.
For OPEN//77, these options are DOM content rather than a browser popup. Portal content defaults outside a scoped island; use portalProps to choose a destination inside the intended flavor scope when necessary. Keep WebUI input focus while the list is open.
SbCombobox
Section titled “SbCombobox”SbCombobox wraps Reka ComboboxRoot for choosing a value by searching item text. Its model is the selected item or selected items; the input’s search text is separate.
<script setup lang="ts">import { SbCombobox, SbComboboxAnchor, SbComboboxInput, SbComboboxTrigger, SbComboboxCancel, SbComboboxContent, SbComboboxGroup, SbComboboxLabel, SbComboboxItem, SbComboboxEmpty, SbProvider,} from '@static-bloom/vue'const contacts = ['Alex Morgan', 'Ren Brooks', 'Sam Vale'] as const</script>
<template> <SbProvider> <SbCombobox class="w-full max-w-sm"> <SbComboboxAnchor> <SbComboboxInput aria-label="Dispatch contact" placeholder="Search contacts" /> <SbComboboxCancel label="Clear contact" /><SbComboboxTrigger label="Show contacts" /> </SbComboboxAnchor> <SbComboboxContent aria-label="Contacts"> <SbComboboxGroup> <SbComboboxLabel>Available crew</SbComboboxLabel> <SbComboboxItem v-for="contact in contacts" :key="contact" :value="contact">{{ contact }}</SbComboboxItem> </SbComboboxGroup> <SbComboboxEmpty>No matching contacts.</SbComboboxEmpty> </SbComboboxContent> </SbCombobox> </SbProvider></template>Anatomy
Section titled “Anatomy”<SbCombobox> <SbComboboxAnchor> <SbComboboxInput /> <SbComboboxCancel /> <SbComboboxTrigger /> </SbComboboxAnchor> <SbComboboxContent> <SbComboboxGroup> <SbComboboxLabel /> <SbComboboxItem value="Alex Morgan" /> </SbComboboxGroup> <SbComboboxSeparator /> <SbComboboxEmpty /> </SbComboboxContent></SbCombobox>| Component / prop | Type | Default | Effect |
|---|---|---|---|
Root class |
HTMLAttributes['class'] |
unset | Merged root styling; other props are ComboboxRootProps |
Input size |
'sm' | 'md' | 'lg' |
'md' |
Input dimensions |
Input invalid |
boolean |
false |
Invalid styling and aria-invalid |
Trigger label |
string |
'Show options' |
Trigger accessible name |
Cancel label |
string |
'Clear selection' |
Clear button accessible name |
Content position |
ComboboxContentProps['position'] |
'popper' |
Popup positioning mode |
Content sideOffset |
number |
4 |
Anchor distance |
Content align |
ComboboxContentProps['align'] |
'start' |
Popup alignment |
Content portalProps |
ComboboxPortalProps |
unset | Portal configuration |
The remaining Reka props/emits pass through. Use the root’s multiple mode and an array model for multiple selections. Filtering matches item text; include SbComboboxEmpty for no matches. Give the input a label and keep clear/show button names specific when several comboboxes share a page.
ArrowDown opens and navigates; Enter selects the highlighted item. Escape closes with focus on the input. The input exposes aria-expanded; items expose selection and disabled states. The content renders through a portal, so in-game pages avoid native popup limitations. Place it inside a suitable flavor scope through portalProps when the page flavor differs from the island.
SbAutocomplete
Section titled “SbAutocomplete”SbAutocomplete wraps Reka AutocompleteRoot. Its v-model is the typed string, including text absent from the suggestions; selecting a suggestion fills that text value.
Destination: Not set
<script setup lang="ts">import { ref } from 'vue'import { SbAutocomplete, SbAutocompleteAnchor, SbAutocompleteInput, SbAutocompleteTrigger, SbAutocompleteCancel, SbAutocompleteContent, SbAutocompleteItem, SbAutocompleteEmpty, SbProvider,} from '@static-bloom/vue'const destination = ref('')</script>
<template> <SbProvider> <div class="w-full max-w-sm space-y-2"> <SbAutocomplete v-model="destination"> <SbAutocompleteAnchor> <SbAutocompleteInput aria-label="Route destination" placeholder="Enter a destination" /> <SbAutocompleteCancel label="Clear destination" /><SbAutocompleteTrigger label="Show destinations" /> </SbAutocompleteAnchor> <SbAutocompleteContent aria-label="Destinations"> <SbAutocompleteItem value="Harbor">Harbor</SbAutocompleteItem> <SbAutocompleteItem value="North depot">North depot</SbAutocompleteItem> <SbAutocompleteEmpty >No suggestions. You can keep your typed destination.</SbAutocompleteEmpty > </SbAutocompleteContent> </SbAutocomplete> <p class="text-sm text-fg-muted">Destination: {{ destination || 'Not set' }}</p> </div> </SbProvider></template>Anatomy
Section titled “Anatomy”<SbAutocomplete> <SbAutocompleteAnchor> <SbAutocompleteInput /> <SbAutocompleteCancel /> <SbAutocompleteTrigger /> </SbAutocompleteAnchor> <SbAutocompleteContent> <SbAutocompleteGroup> <SbAutocompleteLabel /> <SbAutocompleteItem value="Harbor" /> </SbAutocompleteGroup> <SbAutocompleteSeparator /> <SbAutocompleteEmpty /> </SbAutocompleteContent></SbAutocomplete>| Component / prop | Type | Default | Effect |
|---|---|---|---|
Root class |
HTMLAttributes['class'] |
unset | Root styling; remaining props/emits are AutocompleteRootProps/emits |
Input size |
'sm' | 'md' | 'lg' |
'md' |
Input dimensions |
Input invalid |
boolean |
false |
Invalid appearance and aria-invalid |
Trigger label |
string |
'Show options' |
Accessible trigger name |
Cancel label |
string |
'Clear selection' |
Accessible clear name |
Content position |
ComboboxContentProps['position'] |
'popper' |
Positioning mode |
Content sideOffset |
number |
4 |
Anchor distance |
Content align |
ComboboxContentProps['align'] |
'start' |
Alignment |
Content portalProps |
ComboboxPortalProps |
unset | Portal destination and options |
Autocomplete supports free text rather than multiple selected values. Typed text survives Escape. ArrowDown and Enter choose a suggestion; Cancel clears the text and returns focus. SbAutocompleteEmpty explains the empty state without preventing a custom value. Label the input and use a placeholder only as an entry hint.
Suggestions remain DOM-rendered in OPEN//77. If destinations must resolve to server records, resolve and validate the submitted text at that boundary; a suggestion match is not server authorization.
SbListbox
Section titled “SbListbox”SbListbox wraps Reka ListboxRoot for a persistent collection. SbListboxFilter exposes text state; the consumer filters its array, as this example does.
<script setup lang="ts">import { computed, ref } from 'vue'import { SbListbox, SbListboxFilter, SbListboxContent, SbListboxGroup, SbListboxGroupLabel, SbListboxItem, SbProvider,} from '@static-bloom/vue'const query = ref('')const equipment = ['Radio', 'Toolkit', 'Medkit'] as constconst matches = computed(() => equipment.filter((item) => item.toLowerCase().includes(query.value.toLowerCase())),)</script>
<template> <SbProvider> <SbListbox multiple :default-value="['Radio']" class="w-full max-w-sm"> <SbListboxFilter v-model="query" aria-label="Filter equipment" placeholder="Filter equipment" /> <SbListboxContent aria-label="Equipment"> <SbListboxGroup> <SbListboxGroupLabel>Available</SbListboxGroupLabel> <SbListboxItem v-for="item in matches" :key="item" :value="item">{{ item }}</SbListboxItem> </SbListboxGroup> </SbListboxContent> <p v-if="matches.length === 0" role="status" class="text-sm text-fg-muted"> No matching equipment. </p> </SbListbox> </SbProvider></template>Anatomy
Section titled “Anatomy”<SbListbox> <SbListboxFilter /> <SbListboxContent> <SbListboxGroup> <SbListboxGroupLabel /> <SbListboxItem value="Radio" /> </SbListboxGroup> </SbListboxContent></SbListbox>| Component / prop | Type | Default | Effect |
|---|---|---|---|
Root class |
HTMLAttributes['class'] |
unset | Root styling; other props/emits are ListboxRootProps/emits |
Filter size |
'sm' | 'md' | 'lg' |
'md' |
Filter dimensions |
Filter invalid |
boolean |
false |
Invalid appearance and aria-invalid |
Use the root’s v-model for selected values, and a separate filter v-model for the query. With multiple, the root model is a collection and the listbox exposes aria-multiselectable. Selection toggles options; disabled roots block selection. ArrowDown and Enter navigate and select; typeahead skips disabled items.
Name the listbox content as well as the filter. The visible options are already in the document, with no popup or focus trap. In-game inventory filters should keep keyboard focus in the filter while typing and provide a visible empty result message, as shown here.