Skip to content

Overlays & menus

Every overlay in this family is a Reka UI primitive whose content is portalled into <body>, or into the element set by SbProvider’s teleport-to. Nothing uses a browser-drawn popup, a native <dialog> or a title tooltip, so they all render in OPEN//77’s in-game browser.

Root components (SbDialog, SbPopover, SbDropdownMenu, …) forward every Reka root prop and emit, including v-model:open. Triggers, closes and anchors accept as-child, so you can use an SbButton as the trigger.

A modal window for a focused task, wrapping Reka Dialog. SbDialogContent renders the portal, a blurred backdrop and the panel, with a close button in the top-end corner.

No answer yet
DialogDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbButton,
SbDialog,
SbDialogBody,
SbDialogClose,
SbDialogContent,
SbDialogDescription,
SbDialogFooter,
SbDialogHeader,
SbDialogTitle,
SbDialogTrigger,
SbProvider,
} from '@static-bloom/vue'
const open = ref(false)
const answer = ref('No answer yet')
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<SbDialog v-model:open="open">
<SbDialogTrigger as-child>
<SbButton>View contract</SbButton>
</SbDialogTrigger>
<SbDialogContent>
<SbDialogHeader>
<SbDialogTitle>Courier run: Harbor district</SbDialogTitle>
<SbDialogDescription
>Deliver a sealed case to the dockside depot before dawn.</SbDialogDescription
>
</SbDialogHeader>
<SbDialogBody>
<p class="text-fg-muted">
Payout 4,800. The route crosses two checkpoints; a clean vehicle is advised.
</p>
</SbDialogBody>
<SbDialogFooter>
<SbDialogClose as-child>
<SbButton variant="ghost" @click="answer = 'Declined'">Decline</SbButton>
</SbDialogClose>
<SbDialogClose as-child>
<SbButton @click="answer = 'Accepted'">Accept contract</SbButton>
</SbDialogClose>
</SbDialogFooter>
</SbDialogContent>
</SbDialog>
<output class="text-sm text-fg-muted">{{ answer }}</output>
</div>
</SbProvider>
</template>
Anatomy
SbDialog
├─ SbDialogTrigger
└─ SbDialogContent
├─ SbDialogHeader
│ ├─ SbDialogTitle
│ └─ SbDialogDescription
├─ SbDialogBody
└─ SbDialogFooter
└─ SbDialogClose
SbDialogContent prop Type Default Effect
size 'sm' | 'md' | 'lg' | 'xl' | 'full' 'md' Maximum width 24rem, 32rem, 48rem, 64rem, or the whole viewport
showClose boolean true Renders the icon close button
closeLabel string 'Close' Accessible name of the close button
class HTMLAttributes['class'] none Merged onto the panel

SbDialog takes Reka’s open, defaultOpen and modal. The other Reka DialogContent props and emits (@escape-key-down, @pointer-down-outside, …) pass through.

Keyboard and accessibility. The panel has role="dialog", labelled by the title and described by the description. Opening focuses the first focusable element; Tab and Shift+Tab stay inside; Escape or a click on the backdrop closes it and returns focus to the trigger.

A confirmation that interrupts, wrapping Reka AlertDialog. It cannot be dismissed by clicking outside, and focus starts on the safe choice.

3 vehicles in the garage
AlertDialogDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbAlertDialog,
SbAlertDialogAction,
SbAlertDialogCancel,
SbAlertDialogContent,
SbAlertDialogDescription,
SbAlertDialogFooter,
SbAlertDialogHeader,
SbAlertDialogTitle,
SbAlertDialogTrigger,
SbButton,
SbProvider,
} from '@static-bloom/vue'
const vehicles = ref(3)
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<SbAlertDialog>
<SbAlertDialogTrigger as-child>
<SbButton intent="danger" variant="outline" :disabled="vehicles === 0"
>Scrap vehicle</SbButton
>
</SbAlertDialogTrigger>
<SbAlertDialogContent intent="danger">
<SbAlertDialogHeader>
<SbAlertDialogTitle>Scrap this vehicle?</SbAlertDialogTitle>
<SbAlertDialogDescription
>Its upgrades are lost and the garage slot is freed. This cannot be
undone.</SbAlertDialogDescription
>
</SbAlertDialogHeader>
<SbAlertDialogFooter>
<SbAlertDialogCancel>Keep it</SbAlertDialogCancel>
<SbAlertDialogAction intent="danger" @click="vehicles -= 1">Scrap</SbAlertDialogAction>
</SbAlertDialogFooter>
</SbAlertDialogContent>
</SbAlertDialog>
<output class="text-sm text-fg-muted">{{ vehicles }} vehicles in the garage</output>
</div>
</SbProvider>
</template>
Anatomy
SbAlertDialog
├─ SbAlertDialogTrigger
└─ SbAlertDialogContent
├─ SbAlertDialogHeader
│ ├─ SbAlertDialogTitle
│ └─ SbAlertDialogDescription
└─ SbAlertDialogFooter
├─ SbAlertDialogCancel
└─ SbAlertDialogAction
Part Prop Type Default Effect
SbAlertDialogContent size 'sm' | 'md' | 'lg' 'sm' Maximum width 24rem, 32rem or 48rem
intent 'primary' | 'secondary' | 'neutral' | 'info' | 'success' | 'warning' | 'danger' 'primary' Colors the top hairline (primary is the bloom gradient)
SbAlertDialogAction intent same as above 'primary' Color of the solid confirming button
SbAlertDialogCancel class HTMLAttributes['class'] none A ghost button

Keyboard and accessibility. The panel has role="alertdialog" with its description. SbAlertDialogCancel receives focus when it opens, so Enter never confirms by accident. Escape closes it and restores focus; a pointer down outside does not. The action runs its click handler, then closes the dialog.

A dialog docked to an edge of the viewport, for settings and secondary panels. Only SbSheetContent differs from the dialog: SbSheet, SbSheetTrigger, SbSheetHeader, SbSheetBody, SbSheetFooter, SbSheetTitle, SbSheetDescription and SbSheetClose are the dialog parts under sheet names.

SheetDemo.vue
<script setup lang="ts">
import {
SbButton,
SbProvider,
SbSheet,
SbSheetBody,
SbSheetClose,
SbSheetContent,
SbSheetDescription,
SbSheetFooter,
SbSheetHeader,
SbSheetTitle,
SbSheetTrigger,
} from '@static-bloom/vue'
const sides = ['left', 'right'] as const
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-3">
<SbSheet v-for="side in sides" :key="side">
<SbSheetTrigger as-child>
<SbButton variant="outline">Crew roster ({{ side }})</SbButton>
</SbSheetTrigger>
<SbSheetContent :side="side">
<SbSheetHeader>
<SbSheetTitle>Crew roster</SbSheetTitle>
<SbSheetDescription>Four members online, two on a job.</SbSheetDescription>
</SbSheetHeader>
<SbSheetBody>
<ul class="grid gap-2 text-sm text-fg-muted">
<li>Driver: available</li>
<li>Netrunner: on a job</li>
<li>Medic: available</li>
<li>Fixer: on a job</li>
</ul>
</SbSheetBody>
<SbSheetFooter>
<SbSheetClose as-child>
<SbButton>Done</SbButton>
</SbSheetClose>
</SbSheetFooter>
</SbSheetContent>
</SbSheet>
</div>
</SbProvider>
</template>
SbSheetContent prop Type Default Effect
side 'top' | 'right' | 'bottom' | 'left' 'right' Edge it docks to; side panels are at most 26rem wide
showClose boolean true Renders the icon close button
closeLabel string 'Close' Accessible name of the close button
class HTMLAttributes['class'] none Merged onto the panel

Keyboard behavior is the dialog’s: focus is trapped, and Escape or the close button closes the sheet and returns focus to the trigger.

A swipeable panel, wrapping Reka Drawer. It slides from the bottom by default and follows the pointer while it is dragged.

DrawerDemo.vue
<script setup lang="ts">
import {
SbButton,
SbDrawer,
SbDrawerBody,
SbDrawerClose,
SbDrawerContent,
SbDrawerDescription,
SbDrawerFooter,
SbDrawerHeader,
SbDrawerTitle,
SbDrawerTrigger,
SbProvider,
} from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbDrawer>
<SbDrawerTrigger as-child>
<SbButton variant="outline">Open stash</SbButton>
</SbDrawerTrigger>
<SbDrawerContent show-close>
<SbDrawerHeader>
<SbDrawerTitle>Stash</SbDrawerTitle>
<SbDrawerDescription
>12 of 40 slots used. Swipe down or press Escape to close.</SbDrawerDescription
>
</SbDrawerHeader>
<SbDrawerBody>
<p class="text-fg-muted">Medkits, spare ammunition and a sealed data shard.</p>
</SbDrawerBody>
<SbDrawerFooter>
<SbDrawerClose as-child>
<SbButton>Done</SbButton>
</SbDrawerClose>
</SbDrawerFooter>
</SbDrawerContent>
</SbDrawer>
</SbProvider>
</template>
Anatomy
SbDrawer
├─ SbDrawerTrigger
└─ SbDrawerContent
├─ SbDrawerHandle (rendered for you unless show-handle is false)
├─ SbDrawerHeader
│ ├─ SbDrawerTitle
│ └─ SbDrawerDescription
├─ SbDrawerBody
└─ SbDrawerFooter
└─ SbDrawerClose
Part Prop Type Default Effect
SbDrawer swipeDirection 'down' | 'up' | 'left' | 'right' 'down' Edge and dismiss direction: bottom sheet, top sheet, side panels (Reka)
modal boolean | 'trap-focus' true 'trap-focus' keeps focus inside but lets the page take pointer input (Reka)
snapPoints (number | string)[] none Snap positions: fractions, pixels, or CSS lengths (Reka)
SbDrawerContent showHandle boolean true Renders the grab bar on the edge facing the page
showClose boolean false Renders an icon close button, useful without touch input
closeLabel string 'Close' Accessible name of the close button

The other Reka DrawerRoot props (v-model:open, v-model:snapPoint, snapToSequentialPoints) and DrawerContent emits pass through. The handle is decorative (aria-hidden).

Keyboard and accessibility. The drawer opens from its trigger as a described dialog; Escape closes it and restores focus. Swiping is pointer-only, so keep a close control (show-close or an SbDrawerClose) for keyboard and controller users.

A non-modal panel anchored to its trigger, wrapping Reka Popover. Use it for small forms and details that do not need a dialog.

PopoverDemo.vue
<script setup lang="ts">
import {
SbButton,
SbPopover,
SbPopoverClose,
SbPopoverContent,
SbPopoverTrigger,
SbProvider,
} from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<SbPopover>
<SbPopoverTrigger as-child>
<SbButton variant="outline">Route details</SbButton>
</SbPopoverTrigger>
<SbPopoverContent aria-label="Route details" arrow show-close class="grid gap-2">
<p class="font-display font-semibold">Harbor loop</p>
<p class="text-sm text-fg-muted">6.4 km, two checkpoints, est. 9 minutes.</p>
<SbPopoverClose as-child>
<SbButton size="sm">Set waypoint</SbButton>
</SbPopoverClose>
</SbPopoverContent>
</SbPopover>
</SbProvider>
</template>
Anatomy
SbPopover
├─ SbPopoverAnchor (optional: position against another element)
├─ SbPopoverTrigger
└─ SbPopoverContent
└─ SbPopoverClose (optional)
SbPopoverContent prop Type Default Effect
size 'sm' | 'md' | 'lg' 'md' Minimum width 12rem, 16rem or 22rem
variant 'solid' | 'glass' 'solid' glass blurs what is behind the panel, for HUD overlays
arrow boolean false Draws an arrow pointing at the trigger
showClose boolean false Renders an icon close button in the top-end corner
closeLabel string 'Close' Accessible name of the close button
sideOffset number 6 Distance from the trigger in px (Reka)
arrowPadding number 8 Keeps the arrow away from the corners (Reka)
class HTMLAttributes['class'] none Merged onto the panel

side, align, collision options and the other Reka PopoverContent props and emits pass through.

Keyboard and accessibility. The trigger toggles aria-expanded; opening moves focus into the content, and Escape closes it and returns focus to the trigger. Give the content an aria-label or a heading.

A short label shown on hover or keyboard focus, wrapping Reka Tooltip. It relies on the TooltipProvider inside SbProvider, whose tooltip-delay (300 ms) and tooltip-skip-delay (200 ms) set the timing; never add a second provider.

TooltipDemo.vue
<script setup lang="ts">
import { Radar } from '@lucide/vue'
import {
SbButton,
SbProvider,
SbTooltip,
SbTooltipContent,
SbTooltipTrigger,
} from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-3">
<SbTooltip>
<SbTooltipTrigger as-child>
<SbButton variant="outline" icon aria-label="Scan area">
<Radar aria-hidden="true" />
</SbButton>
</SbTooltipTrigger>
<SbTooltipContent>Scan area (Q)</SbTooltipContent>
</SbTooltip>
<SbTooltip>
<SbTooltipTrigger as-child>
<SbButton variant="ghost">Fast travel</SbButton>
</SbTooltipTrigger>
<SbTooltipContent side="bottom">Unlocks after the first job</SbTooltipContent>
</SbTooltip>
</div>
</SbProvider>
</template>
Anatomy
SbTooltip
├─ SbTooltipTrigger
└─ SbTooltipContent
SbTooltipContent prop Type Default Effect
arrow boolean true Draws an arrow pointing at the trigger
sideOffset number 6 Distance from the trigger in px (Reka)
arrowPadding number 6 Keeps the arrow off the corners (Reka)
class HTMLAttributes['class'] none Merged onto the bubble

SbTooltip forwards Reka’s root props, such as delayDuration and disabled.

Keyboard and accessibility. Keyboard focus opens the tooltip, and the tooltip describes its trigger. Escape hides it while the trigger keeps focus. Put only a label in a tooltip; an icon-only trigger still needs its own aria-label.

A preview card for sighted pointer users, wrapping Reka HoverCard: it opens after the pointer rests on the trigger (700 ms) and closes 300 ms after it leaves.

Equipped: Thermal katana

HoverCardDemo.vue
<script setup lang="ts">
import { SbHoverCard, SbHoverCardContent, SbHoverCardTrigger, SbProvider } from '@static-bloom/vue'
</script>
<template>
<SbProvider>
<p class="text-sm text-fg-muted">
Equipped:
<SbHoverCard>
<SbHoverCardTrigger href="#sbhovercard" class="text-primary underline underline-offset-4"
>Thermal katana</SbHoverCardTrigger
>
<SbHoverCardContent arrow class="grid gap-1">
<p class="font-display font-semibold text-fg">Thermal katana</p>
<p class="text-sm text-fg-muted">
Melee, tier 4. Burn damage on charged strikes. Value 2,600.
</p>
</SbHoverCardContent>
</SbHoverCard>
</p>
</SbProvider>
</template>
Anatomy
SbHoverCard
├─ SbHoverCardTrigger (renders a link)
└─ SbHoverCardContent
SbHoverCardContent prop Type Default Effect
variant 'solid' | 'glass' 'solid' glass blurs what is behind the card
arrow boolean false Draws an arrow pointing at the trigger
sideOffset number 8 Distance from the trigger in px (Reka)
arrowPadding number 8 Keeps the arrow off the corners (Reka)
class HTMLAttributes['class'] none Merged onto the card

SbHoverCard forwards Reka’s openDelay, closeDelay and the other root props.

Keyboard and accessibility. Keyboard focus on the trigger opens the card too. The card is supplementary: put nothing in it that is not reachable elsewhere.

A menu of actions opened from a button, wrapping Reka DropdownMenu, with items, checkbox and radio items, labels, groups, separators, shortcuts and submenus.

Last action: none · tracked: true · priority: normal
DropdownMenuDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbButton,
SbDropdownMenu,
SbDropdownMenuCheckboxItem,
SbDropdownMenuContent,
SbDropdownMenuGroup,
SbDropdownMenuItem,
SbDropdownMenuLabel,
SbDropdownMenuRadioGroup,
SbDropdownMenuRadioItem,
SbDropdownMenuSeparator,
SbDropdownMenuShortcut,
SbDropdownMenuSub,
SbDropdownMenuSubContent,
SbDropdownMenuSubTrigger,
SbDropdownMenuTrigger,
SbProvider,
} from '@static-bloom/vue'
const last = ref('none')
const tracked = ref(true)
const priority = ref('normal')
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<SbDropdownMenu>
<SbDropdownMenuTrigger as-child>
<SbButton variant="outline">Job actions</SbButton>
</SbDropdownMenuTrigger>
<SbDropdownMenuContent>
<SbDropdownMenuLabel>Courier run</SbDropdownMenuLabel>
<SbDropdownMenuGroup>
<SbDropdownMenuItem @select="last = 'accepted'">
Accept
<SbDropdownMenuShortcut>Enter</SbDropdownMenuShortcut>
</SbDropdownMenuItem>
<SbDropdownMenuItem disabled>Hire backup</SbDropdownMenuItem>
<SbDropdownMenuSub>
<SbDropdownMenuSubTrigger>Share with</SbDropdownMenuSubTrigger>
<SbDropdownMenuSubContent>
<SbDropdownMenuItem @select="last = 'shared with crew'">Crew</SbDropdownMenuItem>
<SbDropdownMenuItem @select="last = 'shared with fixer'">Fixer</SbDropdownMenuItem>
</SbDropdownMenuSubContent>
</SbDropdownMenuSub>
</SbDropdownMenuGroup>
<SbDropdownMenuSeparator />
<SbDropdownMenuCheckboxItem v-model="tracked">Track on map</SbDropdownMenuCheckboxItem>
<SbDropdownMenuSeparator />
<SbDropdownMenuLabel>Priority</SbDropdownMenuLabel>
<SbDropdownMenuRadioGroup v-model="priority">
<SbDropdownMenuRadioItem value="normal">Normal</SbDropdownMenuRadioItem>
<SbDropdownMenuRadioItem value="urgent">Urgent</SbDropdownMenuRadioItem>
</SbDropdownMenuRadioGroup>
<SbDropdownMenuSeparator />
<SbDropdownMenuItem intent="danger" @select="last = 'abandoned'"
>Abandon</SbDropdownMenuItem
>
</SbDropdownMenuContent>
</SbDropdownMenu>
<output class="text-sm text-fg-muted"
>Last action: {{ last }} · tracked: {{ tracked }} · priority: {{ priority }}</output
>
</div>
</SbProvider>
</template>
Anatomy
SbDropdownMenu
├─ SbDropdownMenuTrigger
└─ SbDropdownMenuContent
├─ SbDropdownMenuLabel
├─ SbDropdownMenuGroup
│ ├─ SbDropdownMenuItem
│ │ └─ SbDropdownMenuShortcut
│ └─ SbDropdownMenuSub
│ ├─ SbDropdownMenuSubTrigger
│ └─ SbDropdownMenuSubContent
├─ SbDropdownMenuSeparator
├─ SbDropdownMenuCheckboxItem
└─ SbDropdownMenuRadioGroup
└─ SbDropdownMenuRadioItem
Part Prop Type Default Effect
SbDropdownMenu modal boolean false true blocks outside interaction and hides the page from assistive tech
SbDropdownMenuContent, …SubContent variant 'solid' | 'glass' 'solid' glass blurs what is behind the menu
sideOffset number 4 Distance from the trigger (Reka)
SbDropdownMenuSubContent alignOffset number -5 Lines the first sub item up with its sub trigger (Reka)
SbDropdownMenuItem intent 'default' | 'danger' 'default' danger paints a destructive item
SbDropdownMenuItem, …Label, …SubTrigger inset boolean false Indents the label to line up with checkbox and radio items

Items emit @select; SbDropdownMenuCheckboxItem binds v-model (boolean) and SbDropdownMenuRadioGroup binds v-model (the selected value). The other Reka props and emits pass through.

modal defaults to false here, unlike in Reka: a modal menu sets aria-hidden on the rest of the page while its focusable trigger stays inside that hidden tree.

Keyboard. Enter, Space or ArrowDown on the trigger opens the menu on its first item. Arrow keys move between items and skip disabled ones; Enter selects. ArrowRight opens a submenu on its first item and ArrowLeft returns to the sub trigger. Escape or a pointer down outside closes the menu.

The same menu opened by a right-click (or a long press) on an area, wrapping Reka ContextMenu. Its parts mirror the dropdown’s under SbContextMenu* names, with SbContextMenuTrigger as the area.

Right-click this inventory slot Last action: none · favorite: false
ContextMenuDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbContextMenu,
SbContextMenuCheckboxItem,
SbContextMenuContent,
SbContextMenuItem,
SbContextMenuLabel,
SbContextMenuSeparator,
SbContextMenuShortcut,
SbContextMenuSub,
SbContextMenuSubContent,
SbContextMenuSubTrigger,
SbContextMenuTrigger,
SbProvider,
} from '@static-bloom/vue'
const last = ref('none')
const favorite = ref(false)
</script>
<template>
<SbProvider>
<div class="grid w-full max-w-md gap-3">
<SbContextMenu>
<SbContextMenuTrigger
class="flex h-28 items-center justify-center rounded-lg border border-dashed border-line-strong text-sm text-fg-muted"
>
Right-click this inventory slot
</SbContextMenuTrigger>
<SbContextMenuContent>
<SbContextMenuLabel>Medkit</SbContextMenuLabel>
<SbContextMenuItem @select="last = 'used'">
Use
<SbContextMenuShortcut>E</SbContextMenuShortcut>
</SbContextMenuItem>
<SbContextMenuSub>
<SbContextMenuSubTrigger>Move to</SbContextMenuSubTrigger>
<SbContextMenuSubContent>
<SbContextMenuItem @select="last = 'moved to stash'">Stash</SbContextMenuItem>
<SbContextMenuItem @select="last = 'moved to vehicle'"
>Vehicle trunk</SbContextMenuItem
>
</SbContextMenuSubContent>
</SbContextMenuSub>
<SbContextMenuSeparator />
<SbContextMenuCheckboxItem v-model="favorite">Favorite</SbContextMenuCheckboxItem>
<SbContextMenuSeparator />
<SbContextMenuItem intent="danger" @select="last = 'dropped'">Drop</SbContextMenuItem>
</SbContextMenuContent>
</SbContextMenu>
<output class="text-sm text-fg-muted"
>Last action: {{ last }} · favorite: {{ favorite }}</output
>
</div>
</SbProvider>
</template>
Anatomy
SbContextMenu
├─ SbContextMenuTrigger (the area that listens for right-click)
└─ SbContextMenuContent
└─ same items as SbDropdownMenuContent, prefixed SbContextMenu

The props match the dropdown menu’s: modal defaults to false; content and sub content take variant; items take intent and inset; labels and sub triggers take inset; sub content defaults to sideOffset 4 and alignOffset -5.

Keyboard. The menu opens next to the pointer. ArrowDown focuses the first item, Enter selects, ArrowRight and ArrowLeft open and close submenus, Escape closes. A right-click has no keyboard equivalent, so offer every context action somewhere else as well.

A horizontal row of menus, as in a desktop application, wrapping Reka Menubar. Each SbMenubarMenu has a trigger and its content; v-model on SbMenubar holds the value of the open menu.

Last action: none · minimap: true · units: metric
MenubarDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbMenubar,
SbMenubarCheckboxItem,
SbMenubarContent,
SbMenubarItem,
SbMenubarLabel,
SbMenubarMenu,
SbMenubarRadioGroup,
SbMenubarRadioItem,
SbMenubarSeparator,
SbMenubarShortcut,
SbMenubarSub,
SbMenubarSubContent,
SbMenubarSubTrigger,
SbMenubarTrigger,
SbProvider,
} from '@static-bloom/vue'
const last = ref('none')
const showMinimap = ref(true)
const units = ref('metric')
</script>
<template>
<SbProvider>
<div class="grid gap-3">
<SbMenubar>
<SbMenubarMenu value="loadout">
<SbMenubarTrigger>Loadout</SbMenubarTrigger>
<SbMenubarContent>
<SbMenubarItem @select="last = 'new loadout'">
New
<SbMenubarShortcut>Ctrl+N</SbMenubarShortcut>
</SbMenubarItem>
<SbMenubarSub>
<SbMenubarSubTrigger>Recent</SbMenubarSubTrigger>
<SbMenubarSubContent>
<SbMenubarItem @select="last = 'stealth'">Stealth</SbMenubarItem>
<SbMenubarItem @select="last = 'assault'">Assault</SbMenubarItem>
</SbMenubarSubContent>
</SbMenubarSub>
<SbMenubarSeparator />
<SbMenubarItem disabled>Share</SbMenubarItem>
</SbMenubarContent>
</SbMenubarMenu>
<SbMenubarMenu value="view">
<SbMenubarTrigger>View</SbMenubarTrigger>
<SbMenubarContent>
<SbMenubarCheckboxItem v-model="showMinimap">Minimap</SbMenubarCheckboxItem>
<SbMenubarSeparator />
<SbMenubarLabel>Units</SbMenubarLabel>
<SbMenubarRadioGroup v-model="units">
<SbMenubarRadioItem value="metric">Metric</SbMenubarRadioItem>
<SbMenubarRadioItem value="imperial">Imperial</SbMenubarRadioItem>
</SbMenubarRadioGroup>
</SbMenubarContent>
</SbMenubarMenu>
</SbMenubar>
<output class="text-sm text-fg-muted"
>Last action: {{ last }} · minimap: {{ showMinimap }} · units: {{ units }}</output
>
</div>
</SbProvider>
</template>
Anatomy
SbMenubar
└─ SbMenubarMenu (value)
├─ SbMenubarTrigger
└─ SbMenubarContent
└─ same items as SbDropdownMenuContent, prefixed SbMenubar
Part Prop Type Default Effect
SbMenubar class HTMLAttributes['class'] none Merged onto the bar
SbMenubarContent variant 'solid' | 'glass' 'solid' glass blurs what is behind the menu
sideOffset number 6 Distance from the trigger (Reka)
align 'start' | 'center' | 'end' 'start' Aligns the menu with the trigger’s start edge (Reka)
SbMenubarSubContent sideOffset, alignOffset number 4, -5 Sub menu placement (Reka)
SbMenubarItem intent 'default' | 'danger' 'default' danger paints a destructive item
SbMenubarItem, …Label, …SubTrigger inset boolean false Indents the label

Keyboard and accessibility. The bar has the menubar role, and ArrowLeft and ArrowRight move between its triggers. Enter opens a menu on its first item; with a menu open, ArrowRight moves to the next menu. ArrowRight on a sub trigger opens its submenu, ArrowLeft closes it. Escape closes the open menu and returns focus to its trigger.

Short, non-blocking notifications, built on Reka Toast. Mount one SbToaster inside SbProvider (which holds Reka’s ToastProvider), then queue toasts from anywhere with useToast.

0 sold
ToastDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbButton, SbProvider, SbToaster, useToast } from '@static-bloom/vue'
const { toast, dismiss } = useToast()
const sold = ref(0)
function sell(): void {
sold.value += 1
toast({
title: 'Item sold',
description: 'Thermal katana for 2,600.',
intent: 'success',
action: { label: 'Undo', onClick: () => (sold.value -= 1) },
})
}
</script>
<template>
<SbProvider>
<SbToaster />
<div class="flex flex-wrap items-center gap-3">
<SbButton @click="sell">Sell item</SbButton>
<SbButton
intent="warning"
variant="outline"
@click="
toast({
title: 'Heat rising',
description: 'Two patrols are heading your way.',
intent: 'warning',
})
"
>
Warn
</SbButton>
<SbButton variant="ghost" @click="dismiss()">Dismiss all</SbButton>
<output class="text-sm text-fg-muted">{{ sold }} sold</output>
</div>
</SbProvider>
</template>
Anatomy (SbToaster renders one per queued toast)
SbToaster
├─ SbToast (intent, variant)
│ ├─ intent icon
│ ├─ SbToastTitle
│ ├─ SbToastDescription
│ ├─ SbToastAction
│ └─ SbToastClose
└─ viewport (the notifications region)
SbToaster prop Type Default Effect
position 'top-right' | 'top-left' | 'top-center' | 'bottom-right' | 'bottom-left' | 'bottom-center' 'bottom-right' Corner or edge the stack docks to; the newest toast sits nearest the edge
variant 'glass' | 'solid' 'glass' Surface of every toast
label string 'Notifications ({hotkey})' Name of the region; {hotkey} becomes the focus shortcut
closeLabel string 'Dismiss notification' Name of every close button
class HTMLAttributes['class'] none Merged onto the viewport

How long a toast stays, its swipe direction and the queue limit are SbProvider props: toast-duration (5000 ms), toast-swipe-direction ('right') and toast-limit.

The parts are exported for a custom toaster: SbToast takes intent ('primary', 'secondary', 'neutral', 'info', 'success', 'warning', 'danger', default 'primary') and variant, plus Reka’s ToastRoot props; SbToastClose takes label (default 'Dismiss notification'); SbToastAction takes Reka’s required alt-text.

Accessibility. Toasts are announced through a live region: danger and warning toasts interrupt (foreground), the others wait (background). Every toast has a labelled close button, and an action button runs its callback, then closes the toast. The hotkey in label (F8 by default in Reka) moves focus to the region.

  • All of these render in the page; none depends on a browser-drawn popup, a native <dialog> or a title attribute, which OPEN//77’s in-game browser cannot be relied on to show.
  • Portals mount in <body> by default. If an SbProvider scopes a flavor to a subtree, set its teleport-to inside that subtree so menus and dialogs keep the flavor.
  • Menus use the popover layer, so they open above dialogs; toasts use their own --sb-z-toast layer.
  • Pick variant="glass" for popovers, menus and toasts that sit over game imagery; solid keeps maximum contrast.
  • Keep a visible close control on drawers and toasts for controller players, and call window.Open77?.ready() only after mounting, as for any in-game page.