Skip to content

Forms

Each example imports from @static-bloom/vue, wraps its controls in SbProvider, and hydrates when visible. Vue setup explains the styles and provider.

Tables describe the wrapper’s additions and defaults, including theme defaults. Every component accepts a merged class (HTMLAttributes['class'], unset). Unless a table says otherwise, optional flags are false and optional values are unset. The remaining props and declared emits of each named Reka primitive pass through; use its root model with v-model and update:modelValue. Input and textarea expose their own native text model.

SbLabel wraps Reka Label. SbInput renders an input through Primitive, or through FieldControl inside SbField, so labels, validation and field values stay connected.

Radio name: Courier

InputDemo.vue
<script setup lang="ts">
import { ref, useId } from 'vue'
import { SbInput, SbLabel, SbProvider } from '@static-bloom/vue'
const id = useId()
const alias = ref<string | number>('Courier')
</script>
<template>
<SbProvider>
<div class="w-full max-w-sm space-y-2">
<SbLabel :for="id">Call sign</SbLabel>
<SbInput :id="id" v-model="alias" autocomplete="nickname" />
<p class="text-sm text-fg-muted">Radio name: {{ alias }}</p>
</div>
</SbProvider>
</template>
Component / prop Type Default Effect
Label class HTMLAttributes['class'] unset Merged label styling; remaining LabelProps pass through
Input modelValue string | number unset Controlled value
Input defaultValue string | number '' effective fallback Initial uncontrolled value
Input type 'text' | 'email' | 'password' | 'search' | 'number' | 'tel' | 'url' 'text' Native input type
Input size 'sm' | 'md' | 'lg' 'md' Input height and type size
Input invalid boolean false Sets aria-invalid

Native attributes such as id, disabled, readonly, autocomplete and inputmode reach the input. The leading and trailing slots add non-interactive adornments. The model emits update:modelValue with a string or number; a native number input’s text edits are still strings.

Connect SbLabel’s for to the input’s id, or use SbFieldLabel in a field. Clicking the label focuses the input. Native text editing and Tab navigation remain available. Use selection and datetime components for in-game popups; the input’s type union deliberately excludes native date, time and color types.

SbInputGroup wraps Reka Primitive to attach an input to SbInputGroupAddon text. It changes layout without replacing the input’s semantics.

Credits.00
InputGroupDemo.vue
<script setup lang="ts">
import { useId } from 'vue'
import { SbInput, SbInputGroup, SbInputGroupAddon, SbLabel, SbProvider } from '@static-bloom/vue'
const id = useId()
</script>
<template>
<SbProvider>
<div class="w-full max-w-sm space-y-2">
<SbLabel :for="id">Contract budget</SbLabel>
<SbInputGroup>
<SbInputGroupAddon>Credits</SbInputGroupAddon>
<SbInput :id="id" default-value="1250" inputmode="decimal" />
<SbInputGroupAddon>.00</SbInputGroupAddon>
</SbInputGroup>
</div>
</SbProvider>
</template>
<SbInputGroup>
<SbInputGroupAddon />
<SbInput />
<SbInputGroupAddon />
</SbInputGroup>
Prop Type Default Effect
size 'sm' | 'md' | 'lg' 'md' Group input height

Remaining PrimitiveProps pass through. Give the input its own label; addon text is not automatically a description or a label. The addon defaults to as="span". The input remains keyboard editable.

SbTextarea renders a native textarea through Reka Primitive or FieldControl inside a field. autoResize measures the control on mount and input, growing it with the content.

TextareaDemo.vue
<script setup lang="ts">
import { useId } from 'vue'
import { SbLabel, SbProvider, SbTextarea } from '@static-bloom/vue'
const id = useId()
</script>
<template>
<SbProvider>
<div class="w-full max-w-sm space-y-2">
<SbLabel :for="id">Delivery instructions</SbLabel>
<SbTextarea :id="id" auto-resize placeholder="Where should the courier leave the parcel?" />
</div>
</SbProvider>
</template>
Prop Type Default Effect
modelValue string | number unset Controlled text
defaultValue string | number '' effective fallback Initial text
size 'sm' | 'md' | 'lg' 'md' Minimum height and type size
invalid boolean false aria-invalid
autoResize boolean false Content-driven height; disables manual resize

It emits update:modelValue; native textarea attributes pass through. Enter inserts a line break, so do not use it as a form-wide submit shortcut. Label the control and let Tab move to the next field.

SbField wraps Reka FieldRoot, with label, description and error parts. SbForm wraps FormRoot to collect named fields, validate on submission and focus the first invalid control.

Use the name your crew recognizes.

FormDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbButton,
SbField,
SbFieldLabel,
SbFieldDescription,
SbFieldError,
SbForm,
SbInput,
SbProvider,
} from '@static-bloom/vue'
const saved = ref(false)
</script>
<template>
<SbProvider>
<SbForm class="w-full max-w-sm" @form-submit="saved = true">
<SbField name="callSign" required>
<SbFieldLabel>Dispatch call sign</SbFieldLabel>
<SbInput autocomplete="nickname" />
<SbFieldDescription>Use the name your crew recognizes.</SbFieldDescription>
<SbFieldError />
</SbField>
<SbButton type="submit" class="justify-self-start">Save call sign</SbButton>
<p v-if="saved" role="status" class="text-sm text-success">Call sign saved.</p>
</SbForm>
</SbProvider>
</template>
<SbForm>
<SbField name="callSign" required>
<SbFieldLabel />
<SbInput />
<SbFieldDescription />
<SbFieldError />
</SbField>
<SbButton type="submit" />
</SbForm>
Component / prop Type Default Effect
Field orientation 'horizontal' | 'vertical' 'vertical' Field layout
Form layout 'stacked' | 'inline' | 'columns' 'stacked' Form layout

Remaining FieldRootProps, FormRootProps and declared form emits pass through. Put name, required, invalid or disabled on the field when they describe the whole field. The form’s formSubmit event receives valid named values; listen with @form-submit as shown.

The field associates label and description/error ids with its input. Invalid fields expose aria-invalid; disabled fields disable native controls. Required validation focuses the first invalid control on submit. Server errors can be displayed and clear when edited. Validation here is UI feedback; the server still owns acceptance of submitted data.

SbCheckbox composes Reka CheckboxRoot and CheckboxIndicator. SbCheckboxGroup wraps CheckboxGroupRoot for a collection of selected values.

CheckboxDemo.vue
<script setup lang="ts">
import { SbCheckbox, SbCheckboxGroup, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbCheckboxGroup :default-value="['radio']" aria-label="Starting equipment">
<label class="inline-flex min-h-8 items-center gap-3 text-sm"
><SbCheckbox value="radio" aria-label="Radio" />Radio</label
>
<label class="inline-flex min-h-8 items-center gap-3 text-sm"
><SbCheckbox value="toolkit" aria-label="Toolkit" />Toolkit</label
>
</SbCheckboxGroup>
</SbProvider>
</template>
<SbCheckboxGroup>
<SbCheckbox value="radio" />
<SbCheckbox value="toolkit" />
</SbCheckboxGroup>
Component / prop Type Default Effect
Checkbox size 'sm' | 'md' | 'lg' 'md' Checkbox dimensions
Checkbox intent 'primary' | 'secondary' | 'success' | 'danger' 'primary' Checked color
Checkbox invalid boolean false aria-invalid
Group class HTMLAttributes['class'] unset Layout classes; other props/emits are CheckboxGroupRootProps/emits

Checkbox uses Reka’s modelValue/defaultValue, including 'indeterminate'; indeterminate is announced as aria-checked="mixed". Space toggles an enabled checkbox. Group items need distinct values and names; a group label does not label each checkbox. Disabled checkboxes cannot toggle.

SbRadioGroup wraps Reka RadioGroupRoot; SbRadioGroupItem composes RadioGroupItem, RadioGroupIndicator and an associated label for its text slot.

RadioGroupDemo.vue
<script setup lang="ts">
import { SbProvider, SbRadioGroup, SbRadioGroupItem } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbRadioGroup default-value="courier" aria-label="Contract type">
<SbRadioGroupItem value="courier">Courier</SbRadioGroupItem>
<SbRadioGroupItem value="recovery">Recovery</SbRadioGroupItem>
<SbRadioGroupItem value="restricted" disabled>Restricted</SbRadioGroupItem>
</SbRadioGroup>
</SbProvider>
</template>
<SbRadioGroup>
<SbRadioGroupItem value="courier">Courier</SbRadioGroupItem>
<SbRadioGroupItem value="recovery">Recovery</SbRadioGroupItem>
</SbRadioGroup>
Component / prop Type Default Effect
Group class HTMLAttributes['class'] unset Root styling; remaining RadioGroupRootProps/emits pass through
Item size 'sm' | 'md' | 'lg' 'md' Radio dimensions
Item intent 'primary' | 'secondary' | 'success' | 'danger' 'primary' Selected color
Item invalid boolean false aria-invalid

Arrow keys focus and select the next radio; disabled options are unavailable. Use the group’s v-model for the selected value and aria-label or aria-labelledby for the question. Item slots provide their associated visible labels. Set orientation="horizontal" when options flow across a row.

SbSwitch wraps Reka SwitchRoot and supplies SwitchThumb for a binary setting.

SwitchDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbProvider, SbSwitch } from '@static-bloom/vue'
const enabled = ref(true)
</script>
<template>
<SbProvider>
<label class="inline-flex min-h-10 items-center gap-3 text-sm">
<SbSwitch v-model="enabled" aria-label="Dispatch audio" />Dispatch audio
{{ enabled ? 'on' : 'off' }}
</label>
</SbProvider>
</template>
Prop Type Default Effect
size 'sm' | 'md' | 'lg' 'md' Track and thumb dimensions
intent 'primary' | 'secondary' | 'success' | 'danger' 'primary' On-state color
invalid boolean false aria-invalid

Remaining SwitchRootProps and emits pass through. v-model is the on/off value; Space toggles the named switch and updates aria-checked. Disabled switches expose their disabled state. Give it a stable setting name, independent of the current on/off value.

SbSlider wraps Reka SliderRoot and supplies track, range and one thumb per value. Its default slot can replace that composition.

Radio volume: 60

SliderDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbProvider, SbSlider } from '@static-bloom/vue'
const volume = ref<number[] | null>([60])
</script>
<template>
<SbProvider>
<div class="w-full max-w-sm space-y-3">
<p class="text-sm text-fg-muted">Radio volume: {{ volume?.[0] }}</p>
<SbSlider v-model="volume" :thumb-labels="['Radio volume']" />
</div>
</SbProvider>
</template>
Prop Type Default Effect
size 'sm' | 'md' | 'lg' 'md' Track and thumb dimensions
thumbLabels readonly string[] unset Each thumb’s accessible name; fallback Value 1, Value 2, etc.

Remaining SliderRootProps and emits pass through. The model is an array, even for one thumb. Use :default-value="[20, 80]" for a range. ArrowRight changes the value; Home/End move to the bounds. Each thumb exposes aria-valuenow. A disabled vertical slider removes thumbs from tab order. Name both ends of a range distinctly.

For game settings, coordinate pointer focus with the game client so dragging a slider does not also move the camera. Keep a numeric alternative available when exact entry matters.

SbNumberField wraps Reka NumberFieldRoot and supplies decrement, input and increment parts by default. Compose those exported parts yourself when their order or labels need to change.

NumberFieldDemo.vue
<script setup lang="ts">
import { SbNumberField, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="w-full max-w-xs">
<SbNumberField label="Crew capacity" :default-value="3" :min="1" :max="8" />
</div>
</SbProvider>
</template>
<SbNumberField>
<SbNumberFieldDecrement />
<SbNumberFieldInput />
<SbNumberFieldIncrement />
</SbNumberField>
Prop Type Default Effect
size 'sm' | 'md' | 'lg' 'md' Inherited input size
label string 'Value' Root and default input accessible name
invalid boolean false aria-invalid on the default input

Remaining NumberFieldRootProps/emits pass through, including bounds and numeric v-model. Increase/Decrease controls default to the labels Increase and Decrease. ArrowUp increments; Home/End use the configured min/max. The input exposes the spinbutton role. With custom composition, label the input and carry its invalid state yourself.

SbPinInput wraps Reka PinInputRoot. Add one SbPinInputInput per character, with zero-based indexes.

PinInputDemo.vue
<script setup lang="ts">
import { SbPinInput, SbPinInputInput, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbPinInput aria-label="Depot access code" placeholder="-" otp>
<SbPinInputInput
v-for="index in 4"
:key="index"
:index="index - 1"
:aria-label="`Access code digit ${index}`"
/>
</SbPinInput>
</SbProvider>
</template>
<SbPinInput>
<SbPinInputInput :index="0" />
<SbPinInputInput :index="1" />
</SbPinInput>
Component / prop Type Default Effect
Root class HTMLAttributes['class'] unset Root styling; PinInputRootProps and emits pass through
Input size 'sm' | 'md' | 'lg' 'md' Character box dimensions

Entering a character advances focus. The root’s mask uses password inputs, and disabled reaches the inputs. otp enables one-time-code usage. Name the group and its digit controls; use the root model for the character array. A visible PIN UI is not itself an authentication boundary.

SbTagsInput wraps Reka TagsInputRoot for editing a list of values. Text, delete and clear parts keep the displayed tags tied to the root model.

Driving
Repair
TagsInputDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbProvider,
SbTagsInput,
SbTagsInputItem,
SbTagsInputItemText,
SbTagsInputItemDelete,
SbTagsInputInput,
SbTagsInputClear,
} from '@static-bloom/vue'
const skills = ref(['Driving', 'Repair'])
</script>
<template>
<SbProvider>
<SbTagsInput v-model="skills" class="w-full max-w-sm">
<SbTagsInputItem v-for="skill in skills" :key="skill" :value="skill">
<SbTagsInputItemText /><SbTagsInputItemDelete :label="`Remove ${skill}`" />
</SbTagsInputItem>
<SbTagsInputInput aria-label="Crew skills" placeholder="Add skill, then Enter" />
<SbTagsInputClear />
</SbTagsInput>
</SbProvider>
</template>
<SbTagsInput>
<SbTagsInputItem value="Driving">
<SbTagsInputItemText />
<SbTagsInputItemDelete />
</SbTagsInputItem>
<SbTagsInputInput />
<SbTagsInputClear />
</SbTagsInput>
Prop Type Default Effect
size 'sm' | 'md' | 'lg' 'md' Root dimensions

Remaining generic TagsInputRootProps<T>/emits pass through; T is string by default and can also be number, bigint or a record. Enter adds a typed tag. The delete part removes its value, and Clear empties the model. Their default labels are Remove tag and Clear tags; use value-specific removal names. Label the text input separately from its placeholder.

SbEditable wraps Reka EditableRoot for an inline preview/edit cycle. Its exported area, preview, input and trigger parts let the consumer make edit and commit controls explicit.

Harbor crew
EditableDemo.vue
<script setup lang="ts">
import {
SbEditable,
SbEditableArea,
SbEditablePreview,
SbEditableInput,
SbEditableEditTrigger,
SbEditableSubmitTrigger,
SbEditableCancelTrigger,
SbProvider,
} from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbEditable
default-value="Harbor crew"
activation-mode="none"
submit-mode="enter"
role="group"
aria-label="Crew editor"
class="w-full max-w-sm"
>
<SbEditableArea class="basis-full">
<SbEditablePreview class="[&[hidden]]:hidden" />
<SbEditableInput label="Crew name" class="[&[hidden]]:hidden" />
</SbEditableArea>
<SbEditableEditTrigger class="bg-panel [&[hidden]]:hidden" />
<SbEditableSubmitTrigger class="bg-panel [&[hidden]]:hidden" />
<SbEditableCancelTrigger class="bg-panel [&[hidden]]:hidden" />
</SbEditable>
</SbProvider>
</template>
<SbEditable>
<SbEditableArea>
<SbEditablePreview />
<SbEditableInput />
</SbEditableArea>
<SbEditableEditTrigger />
<SbEditableSubmitTrigger />
<SbEditableCancelTrigger />
</SbEditable>
Component / prop Type Default Effect
Root class HTMLAttributes['class'] unset Root styling; EditableRootProps/emits pass through
Input size 'sm' | 'md' | 'lg' 'md' Editor dimensions
Input label string 'Editable text' Input accessible name
Edit / Submit / Cancel trigger label string 'Edit' / 'Submit' / 'Cancel' Accessible name and fallback text

The example explicitly sets activation-mode="none" and submit-mode="enter". Its [&[hidden]]:hidden utilities preserve Reka’s hidden state when component display classes override the browser’s default hidden rule. Edit starts entry, Submit commits, and Escape cancels edits. Give the editor a name describing the record. In-game Escape handling depends on the page’s keep-input configuration; retain the visible Cancel trigger.

SbTagGroup wraps Reka TagGroupRoot for selecting and removing existing tags. Unlike tags input, it has no text-entry field.

Harbor
North depot
Old quarter
TagGroupDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbProvider,
SbTagGroup,
SbTagGroupItem,
SbTagGroupItemText,
SbTagGroupItemDelete,
} from '@static-bloom/vue'
const districts = ref(['Harbor', 'North depot', 'Old quarter'])
function remove(values: unknown[]) {
districts.value = districts.value.filter((value) => !values.includes(value))
}
</script>
<template>
<SbProvider>
<SbTagGroup selection-mode="multiple" aria-label="Active districts" @remove="remove">
<SbTagGroupItem
v-for="district in districts"
:key="district"
:value="district"
:text-value="district"
>
<SbTagGroupItemText>{{ district }}</SbTagGroupItemText>
<SbTagGroupItemDelete :label="`Remove ${district}`" />
</SbTagGroupItem>
</SbTagGroup>
</SbProvider>
</template>
<SbTagGroup>
<SbTagGroupItem value="Harbor">
<SbTagGroupItemText />
<SbTagGroupItemDelete />
</SbTagGroupItem>
</SbTagGroup>
Component / prop Type Default Effect
Root class HTMLAttributes['class'] unset Root styling; remaining TagGroupRootProps/emits pass through
ItemDelete label string 'Remove' Delete control name

The root model tracks selected values. A remove event supplies values for the consumer to remove from its own collection, as shown above. The tests exercise keyboard selection and Delete removal. Label the group and give each item readable text; disable locked tags instead of removing their explanation.

All these controls render in the document and need WebUI focus to receive keyboard input in OPEN//77. Keep game shortcuts from interfering while a text control is focused, and call the platform’s ready hook after the app mounts. See In-game UIs.