Skip to content

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().

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.

TabsDemo.vue
<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>
Anatomy
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.

A stack of sections that expand and collapse, wrapping Reka Accordion. Each trigger shows a chevron that turns when its section opens.

Pick up the case at the depot and keep it sealed.
AccordionDemo.vue
<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>
Anatomy
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.

One section that shows and hides, wrapping Reka Collapsible. Use it for optional details; use SbAccordion for a set of sections.

CollapsibleDemo.vue
<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>
Anatomy
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.

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.

NavigationMenuDemo.vue
<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>
Anatomy
SbNavigationMenu
├─ SbNavigationMenuList
│ ├─ SbNavigationMenuItem (value)
│ │ ├─ SbNavigationMenuTrigger
│ │ └─ SbNavigationMenuContent
│ │ ├─ SbNavigationMenuLink (href, active)
│ │ └─ SbNavigationMenuSub (optional second level)
│ └─ SbNavigationMenuIndicator
└─ SbNavigationMenuViewport

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

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
PaginationDemo.vue
<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 = 4
const 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>
Anatomy
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).

The path from the top of a hierarchy to the current page. It is plain markup on Reka’s Primitive, with no state.

BreadcrumbDemo.vue
<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>
Anatomy
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.

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

Step 2 of 0
StepperDemo.vue
<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>
Anatomy
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.

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
  • Stamina
Selected: none
TreeDemo.vue
<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>
Anatomy
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.

A scroll container with themed, overlay scrollbars, wrapping Reka ScrollArea. You place the viewport and each scrollbar yourself.

  • 01Courier run
  • 02Vehicle recovery
  • 03Escort
  • 04Data pickup
  • 05Courier run
  • 06Vehicle recovery
  • 07Escort
  • 08Data pickup
  • 09Courier run
  • 10Vehicle recovery
  • 11Escort
  • 12Data pickup
  • 13Courier run
  • 14Vehicle recovery
  • 15Escort
  • 16Data pickup
  • 17Courier run
  • 18Vehicle recovery
  • 19Escort
  • 20Data pickup
ScrollAreaDemo.vue
<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>
Anatomy
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.

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.

District intel
Route planning. Focus the grip and use the arrow keys to resize.
SplitterDemo.vue
<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>
Anatomy
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.

  • The navigation menu’s panels and every other part render in the page, so nothing depends on a browser-drawn popup.
  • SbScrollArea draws 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; md is 2.25rem). Accordion triggers are at least 2.75rem tall; tree rows are at least 2.25rem.