Navigation
These components move people between views, sections and steps. Each wraps a Reka UI primitive, forwards its props and emits, and exposes the Reka element through ref. Every part also takes class, merged with cn().
SbTabs
Section titled “SbTabs”Switches between panels in place, wrapping Reka Tabs. Add SbTabsIndicator inside the list for the sliding underline or pill.
Dispatch: a new courier run is waiting at the harbor depot.
<script setup lang="ts">import { ref } from 'vue'import { SbProvider, SbTabs, SbTabsContent, SbTabsIndicator, SbTabsList, SbTabsTrigger,} from '@static-bloom/vue'
const app = ref('messages')</script>
<template> <SbProvider> <SbTabs v-model="app" class="w-full max-w-lg"> <SbTabsList aria-label="Phone apps"> <SbTabsTrigger value="messages">Messages</SbTabsTrigger> <SbTabsTrigger value="contacts">Contacts</SbTabsTrigger> <SbTabsTrigger value="wallet" disabled>Wallet</SbTabsTrigger> <SbTabsIndicator /> </SbTabsList> <SbTabsContent value="messages"> <p class="text-fg-muted">Dispatch: a new courier run is waiting at the harbor depot.</p> </SbTabsContent> <SbTabsContent value="contacts"> <p class="text-fg-muted">Fixer, mechanic, medic, and two crew members.</p> </SbTabsContent> </SbTabs> </SbProvider></template>SbTabs├─ SbTabsList│ ├─ SbTabsTrigger (value)│ └─ SbTabsIndicator└─ SbTabsContent (value)| Prop | Type | Default | Effect |
|---|---|---|---|
modelValue |
string | number |
none | Active tab, v-model |
defaultValue |
string | number |
none | Uncontrolled initial tab (Reka) |
variant |
'underline' | 'pill' |
'underline' |
Underline under the list, or a filled pill |
size |
'sm' | 'md' | 'lg' |
'md' |
Trigger height and text size |
stretch |
boolean |
false |
Triggers share the list width equally |
orientation |
'horizontal' | 'vertical' |
'horizontal' |
Vertical tabs put the list beside the panels (Reka) |
activationMode |
'automatic' | 'manual' |
'automatic' |
manual waits for Enter or Space before switching (Reka) |
unmountOnHide |
boolean |
true |
Removes hidden panels from the DOM (Reka) |
Keyboard and accessibility. The list has the tablist role and each trigger is a tab linked to its tabpanel, with aria-selected on the active one. Arrow keys move along the list, skip disabled tabs and, in automatic mode, activate the tab they land on. Name the list with aria-label. The indicator is aria-hidden.
SbAccordion
Section titled “SbAccordion”A stack of sections that expand and collapse, wrapping Reka Accordion. Each trigger shows a chevron that turns when its section opens.
<script setup lang="ts">import { SbAccordion, SbAccordionContent, SbAccordionItem, SbAccordionTrigger, SbProvider,} from '@static-bloom/vue'</script>
<template> <SbProvider> <SbAccordion type="single" default-value="brief" collapsible class="w-full max-w-lg"> <SbAccordionItem value="brief"> <SbAccordionTrigger>Briefing</SbAccordionTrigger> <SbAccordionContent>Pick up the case at the depot and keep it sealed.</SbAccordionContent> </SbAccordionItem> <SbAccordionItem value="reward"> <SbAccordionTrigger>Reward</SbAccordionTrigger> <SbAccordionContent>4,800 and a new contact at the harbor.</SbAccordionContent> </SbAccordionItem> <SbAccordionItem value="locked" disabled> <SbAccordionTrigger>Classified</SbAccordionTrigger> <SbAccordionContent>Requires district clearance.</SbAccordionContent> </SbAccordionItem> </SbAccordion> </SbProvider></template>SbAccordion└─ SbAccordionItem (value) ├─ SbAccordionTrigger └─ SbAccordionContent| Prop | Type | Default | Effect |
|---|---|---|---|
type |
'single' | 'multiple' |
none | One open item at a time, or any number (Reka) |
modelValue |
string | string[] |
none | Open item(s), v-model |
defaultValue |
string | string[] |
none | Uncontrolled open item(s) (Reka) |
collapsible |
boolean |
false |
With type="single", lets the open item close (Reka) |
variant |
'default' | 'boxed' | 'ghost' |
'default' |
Divided rows, separate panels, or no rules |
disabled |
boolean |
false |
Disables every item; SbAccordionItem takes it too (Reka) |
Keyboard. ArrowDown and ArrowUp move between triggers, Home and End jump to the ends, Space or Enter toggles the focused item. A single accordion closes the open item when another opens.
SbCollapsible
Section titled “SbCollapsible”One section that shows and hides, wrapping Reka Collapsible. Use it for optional details; use SbAccordion for a set of sections.
<script setup lang="ts">import { ref } from 'vue'import { SbCollapsible, SbCollapsibleContent, SbCollapsibleTrigger, SbProvider,} from '@static-bloom/vue'
const open = ref(false)</script>
<template> <SbProvider> <SbCollapsible v-model:open="open" class="w-full max-w-lg"> <SbCollapsibleTrigger>{{ open ? 'Hide' : 'Show' }} intercepted message</SbCollapsibleTrigger> <SbCollapsibleContent> <p class="px-3 py-4 text-fg-muted"> The access terminal is behind the market stalls. Avoid the street cameras. </p> </SbCollapsibleContent> </SbCollapsible> </SbProvider></template>SbCollapsible├─ SbCollapsibleTrigger└─ SbCollapsibleContent| Prop | Type | Default | Effect |
|---|---|---|---|
open |
boolean |
none | Controlled state, v-model:open (Reka) |
defaultOpen |
boolean |
false |
Uncontrolled initial state (Reka) |
disabled |
boolean |
false |
The trigger stops toggling (Reka) |
Keyboard and accessibility. The trigger is a button with aria-expanded and aria-controls; Enter or Space toggles it.
SbNavigationMenu
Section titled “SbNavigationMenu”Site navigation with dropdown panels, wrapping Reka NavigationMenu. Triggers open their content on hover or click; SbNavigationMenuViewport hosts the open panel and SbNavigationMenuIndicator points at its trigger. SbNavigationMenuSub nests a second level inside a panel.
<script setup lang="ts">import { SbNavigationMenu, SbNavigationMenuContent, SbNavigationMenuIndicator, SbNavigationMenuItem, SbNavigationMenuLink, SbNavigationMenuList, SbNavigationMenuTrigger, SbNavigationMenuViewport, SbProvider,} from '@static-bloom/vue'</script>
<template> <SbProvider> <SbNavigationMenu aria-label="City guide"> <SbNavigationMenuList> <SbNavigationMenuItem value="districts"> <SbNavigationMenuTrigger>Districts</SbNavigationMenuTrigger> <SbNavigationMenuContent> <SbNavigationMenuLink href="#sbnavigationmenu" active>Harbor</SbNavigationMenuLink> <SbNavigationMenuLink href="#sbnavigationmenu">Old town</SbNavigationMenuLink> <SbNavigationMenuLink href="#sbnavigationmenu">Industrial zone</SbNavigationMenuLink> </SbNavigationMenuContent> </SbNavigationMenuItem> <SbNavigationMenuItem value="services"> <SbNavigationMenuTrigger>Services</SbNavigationMenuTrigger> <SbNavigationMenuContent> <SbNavigationMenuLink href="#sbnavigationmenu">Garages</SbNavigationMenuLink> <SbNavigationMenuLink href="#sbnavigationmenu">Clinics</SbNavigationMenuLink> </SbNavigationMenuContent> </SbNavigationMenuItem> <SbNavigationMenuIndicator /> </SbNavigationMenuList> <SbNavigationMenuViewport align="start" /> </SbNavigationMenu> </SbProvider></template>SbNavigationMenu├─ SbNavigationMenuList│ ├─ SbNavigationMenuItem (value)│ │ ├─ SbNavigationMenuTrigger│ │ └─ SbNavigationMenuContent│ │ ├─ SbNavigationMenuLink (href, active)│ │ └─ SbNavigationMenuSub (optional second level)│ └─ SbNavigationMenuIndicator└─ SbNavigationMenuViewportSbNavigationMenu forwards Reka’s root props (v-model, delayDuration, orientation, …). SbNavigationMenuLink adds an href prop and takes Reka’s active, which sets aria-current="page".
Keyboard. Tab reaches each trigger; Enter, Space or ArrowDown opens its panel and moves into the links. Escape closes the panel and returns focus to its trigger. Name the menu with aria-label.
SbPagination
Section titled “SbPagination”Page controls for a long list, wrapping Reka Pagination. The list’s slot provides the page items, including ellipses, so you render numbers and gaps yourself.
- Job 01
- Job 02
- Job 03
- Job 04
<script setup lang="ts">import { computed, ref } from 'vue'import { SbPagination, SbPaginationEllipsis, SbPaginationFirst, SbPaginationLast, SbPaginationList, SbPaginationListItem, SbPaginationNext, SbPaginationPrev, SbProvider,} from '@static-bloom/vue'
const jobs = Array.from({ length: 48 }, (_, index) => `Job ${String(index + 1).padStart(2, '0')}`)const page = ref(1)const perPage = 4const visible = computed(() => jobs.slice((page.value - 1) * perPage, page.value * perPage))</script>
<template> <SbProvider> <div class="grid w-full max-w-lg gap-4"> <ul class="m-0 grid list-none gap-1 p-0 text-sm text-fg-muted"> <li v-for="job in visible" :key="job">{{ job }}</li> </ul> <SbPagination v-model:page="page" :total="jobs.length" :items-per-page="perPage" :sibling-count="1" show-edges > <SbPaginationList v-slot="{ items }"> <SbPaginationFirst /> <SbPaginationPrev /> <template v-for="(item, index) in items" :key="index"> <SbPaginationListItem v-if="item.type === 'page'" :value="item.value" /> <SbPaginationEllipsis v-else /> </template> <SbPaginationNext /> <SbPaginationLast /> </SbPaginationList> </SbPagination> </div> </SbProvider></template>SbPagination└─ SbPaginationList (slot: { items }) ├─ SbPaginationFirst ├─ SbPaginationPrev ├─ SbPaginationListItem (value) | SbPaginationEllipsis ├─ SbPaginationNext └─ SbPaginationLast| Part | Prop | Type | Default | Effect |
|---|---|---|---|---|
SbPagination |
itemsPerPage |
number |
required | Items on one page (Reka) |
total |
number |
0 |
Number of items (Reka) | |
page |
number |
none | Current page, v-model:page (Reka) |
|
defaultPage |
number |
1 |
Uncontrolled initial page (Reka) | |
siblingCount |
number |
2 |
Pages shown on each side of the current one (Reka) | |
showEdges |
boolean |
false |
Always shows the first and last page (Reka) | |
size |
'sm' | 'md' | 'lg' |
'md' |
Size of every button | |
disabled |
boolean |
false |
Disables every control (Reka) | |
SbPaginationFirst |
label |
string |
'First Page' |
Accessible name of the icon button |
SbPaginationPrev |
label |
string |
'Previous Page' |
Accessible name of the icon button |
SbPaginationNext |
label |
string |
'Next Page' |
Accessible name of the icon button |
SbPaginationLast |
label |
string |
'Last Page' |
Accessible name of the icon button |
Accessibility. The root renders a <nav> named “Pagination”; pass aria-label to name it after its list when a page has several. The current page carries aria-current="page", and First and Previous are disabled on page 1 (Next and Last on the final page).
SbBreadcrumb
Section titled “SbBreadcrumb”The path from the top of a hierarchy to the current page. It is plain markup on Reka’s Primitive, with no state.
<script setup lang="ts">import { SbBreadcrumb, SbBreadcrumbEllipsis, SbBreadcrumbItem, SbBreadcrumbLink, SbBreadcrumbList, SbBreadcrumbPage, SbBreadcrumbSeparator, SbProvider,} from '@static-bloom/vue'</script>
<template> <SbProvider> <SbBreadcrumb> <SbBreadcrumbList> <SbBreadcrumbItem> <SbBreadcrumbLink href="#sbbreadcrumb">Map</SbBreadcrumbLink> </SbBreadcrumbItem> <SbBreadcrumbSeparator /> <SbBreadcrumbItem> <SbBreadcrumbEllipsis /> </SbBreadcrumbItem> <SbBreadcrumbSeparator /> <SbBreadcrumbItem> <SbBreadcrumbLink href="#sbbreadcrumb">Harbor</SbBreadcrumbLink> </SbBreadcrumbItem> <SbBreadcrumbSeparator /> <SbBreadcrumbItem> <SbBreadcrumbPage>Dockside depot</SbBreadcrumbPage> </SbBreadcrumbItem> </SbBreadcrumbList> </SbBreadcrumb> </SbProvider></template>SbBreadcrumb (nav)└─ SbBreadcrumbList (ol) ├─ SbBreadcrumbItem (li) │ └─ SbBreadcrumbLink (a) | SbBreadcrumbPage | SbBreadcrumbEllipsis └─ SbBreadcrumbSeparator (li)| Part | Prop | Type | Default | Effect |
|---|---|---|---|---|
SbBreadcrumb |
label |
string |
'Breadcrumb' |
aria-label of the navigation landmark |
SbBreadcrumbLink |
href |
string |
none | Link target |
SbBreadcrumbEllipsis |
label |
string |
'More pages' |
Screen-reader text for the collapsed segment |
Each part takes as and as-child, with the defaults shown in the anatomy. SbBreadcrumbPage sets aria-current="page"; separators default to a chevron, are aria-hidden, and accept your own icon in their slot.
SbStepper
Section titled “SbStepper”Progress through ordered steps, wrapping Reka Stepper. The root’s slot exposes nextStep, prevStep, isFirstStep and isLastStep; each item’s slot exposes its state.
Harbor bar, back room
Dockside depot
Before dawn
<script setup lang="ts">import { ref } from 'vue'import { Check } from '@lucide/vue'import { SbButton, SbProvider, SbStepper, SbStepperDescription, SbStepperIndicator, SbStepperItem, SbStepperSeparator, SbStepperTitle, SbStepperTrigger,} from '@static-bloom/vue'
const step = ref(2)const objectives = [ { title: 'Meet the contact', description: 'Harbor bar, back room' }, { title: 'Collect the case', description: 'Dockside depot' }, { title: 'Deliver', description: 'Before dawn' },]</script>
<template> <SbProvider> <SbStepper v-slot="{ nextStep, prevStep, isFirstStep, isLastStep }" v-model="step" aria-label="Job objectives" class="w-full flex-wrap" > <SbStepperItem v-for="(objective, index) in objectives" :key="objective.title" v-slot="{ state }" :step="index + 1" > <SbStepperTrigger> <SbStepperIndicator> <Check v-if="state === 'completed'" class="size-4" aria-hidden="true" /> <span v-else>{{ index + 1 }}</span> </SbStepperIndicator> <SbStepperTitle as="span">{{ objective.title }}</SbStepperTitle> </SbStepperTrigger> <SbStepperDescription>{{ objective.description }}</SbStepperDescription> <SbStepperSeparator v-if="index < objectives.length - 1" /> </SbStepperItem> <div class="flex w-full flex-wrap gap-3 pt-4"> <SbButton variant="ghost" :disabled="isFirstStep" @click="prevStep">Previous</SbButton> <SbButton :disabled="isLastStep" @click="nextStep">Next objective</SbButton> </div> </SbStepper> </SbProvider></template>SbStepper└─ SbStepperItem (step; slot: { state }) ├─ SbStepperTrigger │ ├─ SbStepperIndicator (shows the step number by default) │ └─ SbStepperTitle ├─ SbStepperDescription └─ SbStepperSeparator| Prop | Type | Default | Effect |
|---|---|---|---|
modelValue |
number |
none | Active step, v-model (Reka) |
defaultValue |
number |
1 |
Uncontrolled initial step (Reka) |
orientation |
'horizontal' | 'vertical' |
'horizontal' |
Layout and arrow-key axis (Reka) |
linear |
boolean |
true |
Steps must be completed in order (Reka) |
SbStepperItem takes step (required) and disabled.
Keyboard. Arrow keys move focus between step triggers, Enter or Space activates one. In a linear stepper a later step cannot be reached before the earlier ones; moving forward marks earlier steps completed.
SbTree
Section titled “SbTree”A hierarchical list, wrapping Reka Tree. The root flattens your items and hands them to its slot; render one SbTreeItem per entry. SbTreeItem draws a chevron when you pass has-children and indents by level.
- Driving
- Drift control
- Pursuit evasion
- Hacking
- Stamina
<script setup lang="ts">import { ref } from 'vue'import { SbProvider, SbTree, SbTreeItem } from '@static-bloom/vue'
interface Perk { name: string children?: Perk[]}
const perks: Perk[] = [ { name: 'Driving', children: [{ name: 'Drift control' }, { name: 'Pursuit evasion' }] }, { name: 'Hacking', children: [{ name: 'Camera loop' }, { name: 'Door override' }] }, { name: 'Stamina' },]const selected = ref<Perk>()const expanded = ref(['Driving'])</script>
<template> <SbProvider> <div class="grid w-full max-w-sm gap-3"> <SbTree v-slot="{ flattenItems }" v-model="selected" v-model:expanded="expanded" :items="perks" :get-key="(perk: Perk) => perk.name" aria-label="Perk tree" class="m-0 list-none p-0" > <SbTreeItem v-for="item in flattenItems" :key="item._id" v-bind="item.bind" :has-children="item.hasChildren" > {{ item.value.name }} </SbTreeItem> </SbTree> <output class="text-sm text-fg-muted">Selected: {{ selected?.name ?? 'none' }}</output> </div> </SbProvider></template>SbTree (slot: { flattenItems })└─ SbTreeItem v-for item in flattenItems (v-bind="item.bind", has-children)| Part | Prop | Type | Default | Effect |
|---|---|---|---|---|
SbTree |
items |
T[] |
none | Top-level items (Reka) |
getKey |
(item: T) => string |
required | Unique key of an item (Reka) | |
getChildren |
(item: T) => T[] | undefined |
none | Children of an item; Reka reads children without it |
|
modelValue |
T | T[] |
none | Selection, v-model (Reka) |
|
expanded |
string[] |
none | Keys of open branches, v-model:expanded (Reka) |
|
multiple |
boolean |
false |
Multiple selection (Reka) | |
disabled |
boolean |
false |
Disables the whole tree (Reka) | |
SbTreeItem |
hasChildren |
boolean |
false |
Shows the expand chevron, or a spacer when false |
SbTree and SbTreeItem are generic over your item type. The other Reka TreeRoot and TreeItem props and emits pass through.
Keyboard and accessibility. The tree has the tree role and items carry their level. ArrowDown and ArrowUp move, ArrowRight expands a branch (then enters it), ArrowLeft collapses it (or goes to the parent), Enter selects. Name the tree with aria-label.
SbScrollArea
Section titled “SbScrollArea”A scroll container with themed, overlay scrollbars, wrapping Reka ScrollArea. You place the viewport and each scrollbar yourself.
<script setup lang="ts">import { SbProvider, SbScrollArea, SbScrollAreaScrollbar, SbScrollAreaThumb, SbScrollAreaViewport,} from '@static-bloom/vue'
const dispatches = Array.from({ length: 20 }, (_, index) => ({ id: index + 1, text: ['Courier run', 'Vehicle recovery', 'Escort', 'Data pickup'][index % 4],}))</script>
<template> <SbProvider> <SbScrollArea type="always" class="h-48 w-full max-w-md rounded-md border border-line bg-canvas-subtle" > <SbScrollAreaViewport role="region" aria-label="Dispatch feed"> <ul class="m-0 list-none divide-y divide-line p-0"> <li v-for="item in dispatches" :key="item.id" class="px-4 py-2 text-sm text-fg-muted"> <span class="me-3 tabular-nums text-fg-subtle">{{ String(item.id).padStart(2, '0') }}</span >{{ item.text }} </li> </ul> </SbScrollAreaViewport> <SbScrollAreaScrollbar> <SbScrollAreaThumb /> </SbScrollAreaScrollbar> </SbScrollArea> </SbProvider></template>SbScrollArea├─ SbScrollAreaViewport├─ SbScrollAreaScrollbar (orientation)│ └─ SbScrollAreaThumb└─ SbScrollAreaCorner (when both scrollbars show)| Prop | Type | Default | Effect |
|---|---|---|---|
type |
'auto' | 'always' | 'scroll' | 'hover' | 'glimpse' |
'hover' |
When the scrollbars are visible (Reka) |
scrollHideDelay |
number |
600 |
Delay in ms before scroll and hover bars hide (Reka) |
SbScrollAreaScrollbar takes Reka’s orientation ('vertical' by default).
Keyboard and accessibility. The scrollbars are visual only. Give the viewport role="region" and an aria-label, as in the demo, so it is a named stop in the tab order and arrow keys scroll it.
SbSplitterGroup
Section titled “SbSplitterGroup”Resizable panels, wrapping Reka Splitter. Sizes are percentages of the group unless a panel sets size-unit="px". The handle shows a grip and is a focusable separator.
<script setup lang="ts">import { SbProvider, SbSplitterGroup, SbSplitterPanel, SbSplitterResizeHandle,} from '@static-bloom/vue'</script>
<template> <SbProvider> <div class="h-48 w-full rounded-md border border-line"> <SbSplitterGroup direction="horizontal"> <SbSplitterPanel :default-size="35" :min-size="20" class="bg-canvas-subtle p-4" >District intel</SbSplitterPanel > <SbSplitterResizeHandle label="Resize intel and map" /> <SbSplitterPanel :default-size="65" :min-size="20" class="p-4"> Route planning. Focus the grip and use the arrow keys to resize. </SbSplitterPanel> </SbSplitterGroup> </div> </SbProvider></template>SbSplitterGroup (direction)├─ SbSplitterPanel├─ SbSplitterResizeHandle└─ SbSplitterPanel| Part | Prop | Type | Default | Effect |
|---|---|---|---|---|
SbSplitterGroup |
direction |
'horizontal' | 'vertical' |
required | Panels side by side or stacked (Reka) |
keyboardResizeBy |
number | null |
10 |
Arrow-key step (Reka) | |
autoSaveId |
string | null |
null |
Saves the layout in localStorage (Reka) |
|
SbSplitterPanel |
defaultSize |
number |
none | Initial size (Reka) |
minSize |
number |
10 |
Smallest size (Reka) | |
maxSize |
number |
100 |
Largest size (Reka) | |
collapsible |
boolean |
false |
Collapses below minSize to collapsedSize (Reka) |
|
SbSplitterResizeHandle |
label |
string |
'Resize panels' |
aria-label of the separator |
The group and panel emits (@layout, @resize, @collapse, @expand) pass through.
Keyboard. Tab focuses the handle; arrow keys resize the panels by keyboardResizeBy. Name each handle after what it resizes when a page has several.
In-game notes
Section titled “In-game notes”- The navigation menu’s panels and every other part render in the page, so nothing depends on a browser-drawn popup.
SbScrollAreadraws its own scrollbars, which keeps long lists on themed bars when the in-game browser’s default scrollbars would clash with a HUD; keep<html>and<body>transparent and give the area a panel background.- For controller-driven cursors, prefer
size="lg"pagination (2.75rem, 44 px buttons;mdis 2.25rem). Accordion triggers are at least 2.75rem tall; tree rows are at least 2.25rem.