Skip to content

Actions & HUD

Import these components from @static-bloom/vue. Each example is a separate client:visible island with its own SbProvider; Vue setup covers the shared stylesheet and provider.

The tables list Static Bloom’s additions and overrides. Every component accepts class (HTMLAttributes['class'], unset), merged with the theme. Components wrapping Reka forward the remaining props and declared emits of the named primitive; components built from native elements do not acquire a Reka model or keyboard behavior.

Here, Intent means 'primary' | 'secondary' | 'neutral' | 'info' | 'success' | 'warning' | 'danger'. Boolean flags default to false unless stated otherwise; optional labels default to unset. Defaults below include theme defaults.

SbButton renders Reka Primitive as a native button by default. SbButtonGroup is a labelled div with role="group", joining adjacent buttons without introducing roving focus.

No active job
ButtonDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbButton, SbButtonGroup, SbProvider } from '@static-bloom/vue'
const accepted = ref(false)
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<SbButton @click="accepted = !accepted">Accept job</SbButton>
<SbButtonGroup label="Route actions">
<SbButton variant="outline">Preview</SbButton>
<SbButton variant="outline" disabled>Archive</SbButton>
</SbButtonGroup>
<span role="status" class="text-sm text-fg-muted">{{
accepted ? 'Job accepted' : 'No active job'
}}</span>
</div>
</SbProvider>
</template>
<SbButtonGroup>
<SbButton />
<SbButton />
</SbButtonGroup>
Component / prop Type Default Effect
Button variant 'solid' | 'soft' | 'outline' | 'ghost' | 'link' 'solid' Surface treatment
Button intent Intent 'primary' Semantic color
Button size 'xs' | 'sm' | 'md' | 'lg' 'md' Control height
Button icon, block boolean false Square icon control / full width
Button loading boolean false Disables, sets aria-busy, overlays a spinner while retaining the label
Button disabled boolean false Native disabled state; aria-disabled for other elements
Button as PrimitiveProps['as'] 'button' Rendered element
Button type 'button' | 'submit' | 'reset' 'button' Native button type
Group orientation 'horizontal' | 'vertical' 'horizontal' Attached-button direction
Group label string unset Accessible group name

Tab reaches each enabled button; Enter and Space activate native buttons. Icon-only buttons need an accessible name. With as="a" or as-child, preserve the child’s semantics and handle disabled activation in consumer code; aria-disabled alone is not native disabling.

SbToggle wraps Reka Toggle for a pressed state. SbToggleGroup wraps ToggleGroupRoot; its items inherit the group’s style and size.

ToggleDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbProvider, SbToggle, SbToggleGroup, SbToggleGroupItem } from '@static-bloom/vue'
const muted = ref(false)
const view = ref('map')
</script>
<template>
<SbProvider>
<div class="flex flex-wrap gap-4">
<SbToggle v-model="muted" aria-label="Mute radio">Mute radio</SbToggle>
<SbToggleGroup v-model="view" type="single" variant="outline" aria-label="Route view">
<SbToggleGroupItem value="map">Map</SbToggleGroupItem>
<SbToggleGroupItem value="list">List</SbToggleGroupItem>
</SbToggleGroup>
</div>
</SbProvider>
</template>
<SbToggle />
<SbToggleGroup>
<SbToggleGroupItem value="map" />
</SbToggleGroup>
Component / prop Type Default Effect
Toggle / Group variant 'default' | 'outline' 'default' Toggle surface
Toggle / Group size 'sm' | 'md' | 'lg' 'md' Item dimensions
Toggle modelValue boolean | null unset v-model; emits update:modelValue
Group type 'single' | 'multiple' inferred from value, otherwise 'single' Selection mode; set explicitly for an initially empty group
Group modelValue ToggleGroupRootProps['modelValue'] uncontrolled; initialized from defaultValue Selected item value(s); emits update:modelValue

Space toggles aria-pressed; disabled items do not activate. Group arrow keys move roving focus between items. Give the group an accessible name and each item visible text or aria-label.

SbToolbar wraps Reka ToolbarRoot to group related commands into one tab stop. Use its own button, link and toggle parts so arrow navigation can find them.

ToolbarDemo.vue
<script setup lang="ts">
import {
SbProvider,
SbToolbar,
SbToolbarButton,
SbToolbarLink,
SbToolbarSeparator,
SbToolbarToggleGroup,
SbToolbarToggleItem,
} from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbToolbar aria-label="Dispatch tools" class="flex-wrap">
<SbToolbarToggleGroup type="single" default-value="local" aria-label="Channel">
<SbToolbarToggleItem value="local">Local</SbToolbarToggleItem>
<SbToolbarToggleItem value="crew">Crew</SbToolbarToggleItem>
</SbToolbarToggleGroup>
<SbToolbarSeparator />
<SbToolbarButton>Save</SbToolbarButton>
<SbToolbarLink href="/vue/">Guide</SbToolbarLink>
</SbToolbar>
</SbProvider>
</template>
<SbToolbar>
<SbToolbarToggleGroup>
<SbToolbarToggleItem value="local" />
</SbToolbarToggleGroup>
<SbToolbarSeparator />
<SbToolbarButton />
<SbToolbarLink />
</SbToolbar>
Component / prop Type Default Effect
Toolbar class HTMLAttributes['class'] unset Merged root styling; other props are ToolbarRootProps
ToolbarButton variant, intent, size Button variants above 'ghost', 'neutral', 'sm' Command appearance
ToolbarButton icon boolean false Square command
ToolbarToggleGroup variant, size Toggle variants above 'default', 'sm' Inherited toggle appearance

The root exposes role="toolbar". Tab enters its roving focus group; arrows move between commands, and Enter activates a button. Separators orient across the toolbar’s flow. Set aria-label on the toolbar.

SbBadge uses Reka Primitive as a span for a status or category. It has no interaction or live announcement by itself.

AvailableAwaiting crewCourier
BadgeDemo.vue
<script setup lang="ts">
import { SbBadge, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="flex flex-wrap gap-3">
<SbBadge intent="success" dot>Available</SbBadge>
<SbBadge intent="warning" variant="outline">Awaiting crew</SbBadge>
<SbBadge accent="orchid">Courier</SbBadge>
</div>
</SbProvider>
</template>
Prop Type Default Effect
variant 'solid' | 'soft' | 'outline' 'soft' Surface
intent Intent 'primary' Semantic tone
accent 'plasma' | 'laser' | 'ember' | 'sodium' | 'halogen' | 'acid' | 'coolant' | 'frost' | 'ion' | 'ultraviolet' | 'orchid' | 'chrome' unset Overrides intent
size 'sm' | 'md' | 'lg' 'md' Badge size
dot boolean false Leading color dot
as PrimitiveProps['as'] 'span' Rendered element

Keep the status in text; the dot alone does not convey its meaning to a screen reader.

SbKbd is a native kbd. SbKeyHint composes keycaps and an action label in a span; neither registers a shortcut or wraps an interactive Reka primitive.

Confirm with EnterCtrlKSearch
KeyHintDemo.vue
<script setup lang="ts">
import { SbKbd, SbKeyHint, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<span class="text-sm text-fg-muted">Confirm with <SbKbd>Enter</SbKbd></span>
<SbKeyHint :keys="['Ctrl', 'K']" label="Search" />
</div>
</SbProvider>
</template>
Component / prop Type Default Effect
Both size 'sm' | 'md' | 'lg' 'md' Keycap dimensions
Both highlight boolean false Active key treatment
KeyHint keys readonly string[] required Key combination
KeyHint label string unset Action text; default slot can replace it
KeyHint disabled boolean false Muted state and aria-disabled

Bind the actual shortcut in the app. In OPEN//77, Escape may be consumed by the platform unless keep-input is enabled; do not promise an Escape action without checking that page’s input configuration.

SbCard and its parts use Reka Primitive for a composed content surface. The default card is a div, not a button.

Harbor delivery

Courier contract

Collect the sealed parcel at the south depot.
CardDemo.vue
<script setup lang="ts">
import {
SbButton,
SbCard,
SbCardHeader,
SbCardTitle,
SbCardDescription,
SbCardContent,
SbCardFooter,
SbProvider,
} from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbCard class="w-full max-w-sm">
<SbCardHeader>
<SbCardTitle as="p">Harbor delivery</SbCardTitle>
<SbCardDescription>Courier contract</SbCardDescription>
</SbCardHeader>
<SbCardContent>Collect the sealed parcel at the south depot.</SbCardContent>
<SbCardFooter><SbButton variant="soft">View route</SbButton></SbCardFooter>
</SbCard>
</SbProvider>
</template>
<SbCard>
<SbCardHeader>
<SbCardTitle />
<SbCardDescription />
</SbCardHeader>
<SbCardContent />
<SbCardFooter />
</SbCard>
Prop Type Default Effect
variant 'default' | 'flat' | 'glass' | 'interactive' 'default' Surface and interaction styling
selected boolean false Selected appearance and data-selected
as PrimitiveProps['as'] 'div' Root element

SbCardTitle defaults to h3; choose as to fit the document outline. variant="interactive" only changes styling: use a real button or link when the whole card is actionable, and avoid nested interactive controls in that case.

SbSeparator wraps Reka Separator for horizontal or vertical division, optionally with a centered label.

Dispatch channel Private crew messages
SeparatorDemo.vue
<script setup lang="ts">
import { SbProvider, SbSeparator } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="w-full text-sm text-fg-muted">
Dispatch channel
<SbSeparator label="Crew channel" />
Private crew messages
</div>
</SbProvider>
</template>
Prop Type Default Effect
label string unset Text in a horizontal separator
neon boolean false Gradient tube treatment

Reka’s remaining separator props pass through. It is decorative by default; set :decorative="false" when the separator needs a semantic role. A vertical separator needs a parent with height.

SbAvatar wraps Reka AvatarRoot, with image and fallback parts. SbAvatarGroup is a labelled native group that caps visible children and adds a +N overflow avatar.

ALAlexOnlineRNRen+1 more
AvatarDemo.vue
<script setup lang="ts">
import { SbAvatar, SbAvatarFallback, SbAvatarGroup, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbAvatarGroup :max="2" label="Delivery crew">
<SbAvatar status="online"
><SbAvatarFallback>AL<span class="sr-only">Alex</span></SbAvatarFallback></SbAvatar
>
<SbAvatar
><SbAvatarFallback>RN<span class="sr-only">Ren</span></SbAvatarFallback></SbAvatar
>
<SbAvatar
><SbAvatarFallback>SM<span class="sr-only">Sam</span></SbAvatarFallback></SbAvatar
>
</SbAvatarGroup>
</SbProvider>
</template>
<SbAvatarGroup>
<SbAvatar>
<SbAvatarImage />
<SbAvatarFallback />
</SbAvatar>
</SbAvatarGroup>
Component / prop Type Default Effect
Avatar / Group size 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' 'md' Group size is inherited unless overridden
Avatar square boolean false Rounded square
Avatar status 'online' | 'away' | 'busy' | 'offline' unset Status dot and screen-reader status text
Group max number unset Maximum avatars before overflow count
Group label string unset Accessible group name

The fallback displays when the image cannot load. Supply meaningful alt to SbAvatarImage (its default is empty) and accessible fallback text. For offline game pages, bundle images locally; a group count does not expose hidden members’ names.

SbAspectRatio wraps Reka AspectRatio to reserve media space. It adds only the shared class prop; ratio and the remaining props pass through to Reka.

Route preview · 16:9
AspectRatioDemo.vue
<script setup lang="ts">
import { SbAspectRatio, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="w-full max-w-sm">
<SbAspectRatio :ratio="16 / 9">
<div class="grid size-full place-items-center bg-panel text-sm text-fg-muted">
Route preview · 16:9
</div>
</SbAspectRatio>
</div>
</SbProvider>
</template>
Prop Type Default Effect
class HTMLAttributes['class'] unset Merged surface styling

Set ratio explicitly, as the example does. The wrapper does not replace an image’s alt text and adds no keyboard stop.

SbSkeleton uses Reka Primitive for an aria-hidden placeholder. SbSpinner uses a native span with a status label, or a decorative span when another control owns the announcement.

Loading crew roster
LoadingDemo.vue
<script setup lang="ts">
import { SbProvider, SbSkeleton, SbSpinner } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="flex w-full max-w-sm items-center gap-4">
<SbSkeleton shape="circle" />
<div class="min-w-0 flex-1 space-y-2">
<SbSkeleton shape="text" /><SbSkeleton shape="text" class="w-2/3" />
</div>
<SbSpinner label="Loading crew roster" />
</div>
</SbProvider>
</template>
Component / prop Type Default Effect
Skeleton shape 'block' | 'text' | 'circle' | 'rect' 'block' Placeholder shape
Skeleton static boolean false Disables shimmer
Skeleton as PrimitiveProps['as'] 'div' Placeholder element
Spinner size 'xs' | 'sm' | 'md' | 'lg' | 'xl' 'md' Spinner dimensions
Spinner intent Intent or 'current' 'current' Spinner color
Spinner label string 'Loading' Screen-reader status text
Spinner decorative boolean false Removes status role and hides from assistive technology

Neither takes focus. Keep placeholders close to the eventual content dimensions; name the operation on a non-decorative spinner rather than adding multiple identical status announcements.

SbAlert is a native feedback surface, with title and description parts. It selects role="alert" for warning/danger and role="status" for other intents; it does not wrap a Reka alert dialog.

Dispatch connected

New contracts will appear in the job board.

AlertDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbAlert, SbAlertTitle, SbAlertDescription, SbButton, SbProvider } from '@static-bloom/vue'
const visible = ref(true)
</script>
<template>
<SbProvider>
<SbAlert v-if="visible" dismissible class="w-full" @dismiss="visible = false">
<SbAlertTitle>Dispatch connected</SbAlertTitle>
<SbAlertDescription>New contracts will appear in the job board.</SbAlertDescription>
</SbAlert>
<SbButton v-else variant="outline" @click="visible = true">Restore notice</SbButton>
</SbProvider>
</template>
<SbAlert>
<SbAlertTitle />
<SbAlertDescription />
</SbAlert>
Prop Type Default Effect
variant 'soft' | 'solid' | 'outline' 'soft' Surface
intent Intent 'info' Tone, icon and announcement role
hideIcon boolean false Hides the default leading icon
dismissible boolean false Adds a native dismiss button
dismissLabel string 'Dismiss' Dismiss button name

The dismiss button emits dismiss without removing the alert; the consumer controls visibility. Tab and Enter/Space operate that button. An alert does not trap focus or replace a confirmation dialog.

SbLink uses Reka Primitive as an anchor. External links add an arrow, a screen-reader hint and safe new-tab attributes.

LinkDemo.vue
<script setup lang="ts">
import { SbLink, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="flex flex-wrap gap-4">
<SbLink href="/vue/">Vue setup</SbLink>
<SbLink href="/open77/in-game/" variant="muted">In-game guide</SbLink>
<SbLink disabled>Archived route</SbLink>
</div>
</SbProvider>
</template>
Prop Type Default Effect
variant 'default' | 'muted' | 'subtle' 'default' Link appearance
href string unset Destination
external boolean false _blank plus noopener noreferrer
externalLabel string '(opens in a new tab)' Hidden external-link hint
disabled boolean false Removes href, adds aria-disabled
as PrimitiveProps['as'] 'a' Rendered element

Native links activate with Enter. In-game pages should use app navigation or an explicit bridge action when a browser tab is not meaningful. With as-child, the child owns its content and external hint.

SbProgress wraps Reka ProgressRoot and supplies ProgressIndicator. Pass null or omit modelValue for an indeterminate operation.

ProgressDemo.vue
<script setup lang="ts">
import { SbProgress, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="w-full max-w-sm space-y-4">
<SbProgress :model-value="64" label="Download progress" />
<SbProgress :model-value="null" label="Connecting to dispatch" />
</div>
</SbProvider>
</template>
Prop Type Default Effect
intent Intent 'primary' Indicator tone
size 'xs' | 'sm' | 'md' | 'lg' 'md' Track height
glow boolean false Track bloom
label string unset aria-label

The remaining ProgressRootProps and emits pass through. The indicator uses max or 100 for its visual scale. Determinate progress exposes aria-valuenow; indeterminate progress omits it. This display takes no keyboard input.

SbRating composes Reka RatingRoot, RatingItem and RatingItemIndicator. Readonly mode instead renders a single labelled image role and allows fractional display values.

3 of 5
RatingDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbProvider, SbRating } from '@static-bloom/vue'
const rating = ref(3)
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<SbRating v-model="rating" label="Rate this route" />
<span class="text-sm text-fg-muted">{{ rating }} of 5</span>
</div>
</SbProvider>
</template>
Prop Type Default Effect
size 'sm' | 'md' | 'lg' 'md' Star dimensions
readonly boolean false Display-only role="img"
label string unset (interactive fallback 'Rating') Accessible name
length number 5 Number of stars
step RatingRootProps['step'] 1 Selection step

Remaining Reka rating props/emits pass through, except as and asChild. v-model receives update:modelValue. Arrow keys move between ratings and Space selects; readonly mode has no interactive star stops. Supply a label describing what is rated.

SbTable and its parts use Reka Primitive with native table elements. The root supplies a horizontal scroll wrapper, not a grid keyboard model or a sorting engine.

Available contracts
RouteStatus
HarborOpen
North depotAssigned
Two contracts
TableDemo.vue
<script setup lang="ts">
import {
SbProvider,
SbTable,
SbTableCaption,
SbTableHeader,
SbTableHead,
SbTableBody,
SbTableRow,
SbTableCell,
SbTableFooter,
} from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbTable striped hover>
<SbTableCaption>Available contracts</SbTableCaption>
<SbTableHeader
><SbTableRow
><SbTableHead>Route</SbTableHead><SbTableHead>Status</SbTableHead></SbTableRow
></SbTableHeader
>
<SbTableBody>
<SbTableRow><SbTableCell>Harbor</SbTableCell><SbTableCell>Open</SbTableCell></SbTableRow>
<SbTableRow
><SbTableCell>North depot</SbTableCell><SbTableCell>Assigned</SbTableCell></SbTableRow
>
</SbTableBody>
<SbTableFooter
><SbTableRow
><SbTableCell colspan="2">Two contracts</SbTableCell></SbTableRow
></SbTableFooter
>
</SbTable>
</SbProvider>
</template>
<SbTable>
<SbTableCaption />
<SbTableHeader><SbTableRow><SbTableHead /></SbTableRow></SbTableHeader>
<SbTableBody><SbTableRow><SbTableCell /></SbTableRow></SbTableBody>
<SbTableFooter><SbTableRow><SbTableCell /></SbTableRow></SbTableFooter>
</SbTable>
Component / prop Type Default Effect
Table striped, hover, compact boolean false Alternate rows, hover fill, reduced padding
Table as PrimitiveProps['as'] 'table' Table element
TableHead sort 'ascending' | 'descending' | 'none' unset aria-sort and direction icon
TableRow selected boolean false Selected styling and aria-selected

Keep a caption and column headings. Sorting and row selection require consumer state and keyboard-accessible controls; sort only describes the direction. Avoid turning a clickable row into the only way to open a record.

SbHudPanel uses Reka Primitive as a section; SbNeonText uses it as a span. SbStatBar is a native, labelled role="meter" display with segmented fill. This demo uses the game backdrop to check panel legibility.

Vehicle telemetry

Engine online
Hull
Fuel
HudDemo.vue
<script setup lang="ts">
import { SbHudPanel, SbNeonText, SbProvider, SbStatBar } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbHudPanel title="Vehicle telemetry" title-as="p" status class="w-full max-w-sm">
<div class="space-y-4">
<SbNeonText intent="success">Engine online</SbNeonText>
<SbStatBar label="Hull" :value="78" intent="success" />
<SbStatBar
label="Fuel"
:value="42"
intent="warning"
:segments="20"
value-text="42 percent fuel remaining"
/>
</div>
</SbHudPanel>
</SbProvider>
</template>
<SbHudPanel>
<template #header />
<template #actions />
<SbNeonText />
<SbStatBar><template #icon /></SbStatBar>
</SbHudPanel>

For HudPanel and StatBar, HUD intent is Intent as defined above. NeonText accepts the same values except 'neutral'.

Component / prop Type Default Effect
HudPanel intent HUD intent 'primary' Header and frame tone
HudPanel as PrimitiveProps['as'] 'section' Root element
HudPanel title string unset Optional header title
HudPanel titleAs 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' 'h3' Title element
HudPanel status boolean false Decorative status dot
HudPanel brackets boolean true Corner brackets
HudPanel scanlines, solid boolean false Scanlines / opaque sunken surface instead of glass
StatBar label, value string, number required Meter name and current value
StatBar max, segments number 100, 10 Maximum value and segment count
StatBar intent, size HUD intent, 'sm' | 'md' | 'lg' 'primary', 'md' Tone and dimensions
StatBar glow, critical, hideValue boolean false Numeric glow, critical pulse, hidden numeric readout
StatBar valueText string unset aria-valuetext
NeonText intent HUD intent unset Intent ink and glow
NeonText glow 'text' | 'bloom' | 'none' 'text' Glow treatment
NeonText gradient, flicker boolean false Bloom gradient / tube flicker
NeonText as PrimitiveProps['as'] 'span' Text element

The meter clamps value between zero and max, and exposes its minimum, maximum and current value. These displays do not take focus or announce every telemetry update. The panel’s status dot is decorative; include status text when it matters.

For OPEN//77, put HUD widgets on one WebUI page, keep the document transparent and use sufficiently opaque panel surfaces over game imagery. Use solid when the scene makes glass hard to read. Interactive HUD controls still need page focus from the game client. In-game UIs covers input and bridge setup.