Skip to content

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 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

SelectDemo.vue
<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>
<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 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.

ComboboxDemo.vue
<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>
<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 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

AutocompleteDemo.vue
<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>
<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 wraps Reka ListboxRoot for a persistent collection. SbListboxFilter exposes text state; the consumer filters its array, as this example does.

Available
Radio
Toolkit
Medkit
ListboxDemo.vue
<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 const
const 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>
<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.