Composables & utilities
Besides components, @static-bloom/vue exports two composables, two class helpers and a plugin. Everything on this page is a named export of the package root.
import { StaticBloomPlugin, cn, tv, useFlavor, useToast } from '@static-bloom/vue'useFlavor
Section titled “useFlavor”Reads and sets the flavor. Flavors & theming explains flavors themselves; this section is the reference.
<script setup lang="ts">import { SbButton, SbProvider, useFlavor } from '@static-bloom/vue'
const { flavor, resolvedFlavor, isDark, flavors, setFlavor } = useFlavor()</script>
<template> <SbProvider> <div class="grid gap-3"> <div class="flex flex-wrap gap-2" role="group" aria-label="Flavor"> <SbButton size="sm" :variant="flavor === 'auto' ? 'solid' : 'outline'" :aria-pressed="flavor === 'auto'" @click="setFlavor('auto')" > Auto </SbButton> <SbButton v-for="item in flavors" :key="item.id" size="sm" :variant="flavor === item.id ? 'solid' : 'outline'" :aria-pressed="flavor === item.id" @click="setFlavor(item.id)" > {{ item.label }} </SbButton> </div> <output class="text-sm text-fg-muted"> flavor: {{ flavor }} · resolvedFlavor: {{ resolvedFlavor }} · isDark: {{ isDark }} </output> </div> </SbProvider></template>On this site the buttons drive the page runtime, so the header’s selector and every other island follow.
Returned values
Section titled “Returned values”| Name | Type | Description |
|---|---|---|
flavor |
Readonly<Ref<FlavorPreference>> |
The preference: 'auto', 'dusk', 'midnight', 'blackout' or 'daybreak' |
resolvedFlavor |
ComputedRef<FlavorName> |
The flavor actually applied; 'auto' resolved through the system preference |
isDark |
ComputedRef<boolean> |
Whether resolvedFlavor is a dark flavor (every flavor except Daybreak) |
flavors |
readonly FlavorMeta[] |
{ id, label, dark } for the four flavors |
setFlavor |
(preference: FlavorPreference) => void |
Changes the preference |
Options
Section titled “Options”Options are read by the first useFlavor call in a Vue app, which creates the app’s shared state. Later calls in the same app return that state and ignore their options.
| Option | Type | Default | Effect |
|---|---|---|---|
defaults |
{ dark?: DarkFlavorName; light?: LightFlavorName } |
{ dark: 'midnight', light: 'daybreak' } |
Flavors that 'auto' resolves to |
defaultFlavor |
FlavorPreference |
'auto' |
Preference used when nothing is stored |
storageKey |
string |
'static-bloom:flavor' |
localStorage key of the preference |
storage |
{ getItem; setItem } | null |
window.localStorage |
Where the preference persists; null disables persistence |
target |
HTMLElement | (() => HTMLElement | null) | null |
document.documentElement |
Element that receives the attributes; null applies nothing |
matchMedia |
(query: string) => MediaQueryLike | null |
window.matchMedia |
Source of prefers-color-scheme, replaceable in tests |
runtime |
FlavorRuntimeLike | null |
window.staticBloom |
Page runtime to delegate to; null forces standalone mode |
Two modes
Section titled “Two modes”With a page runtime. When window.staticBloom exists (installed by the Astro integration or the Starlight plugin), the runtime owns <html>, storage and the media query. useFlavor reads its current preference on mount, setFlavor calls staticBloom.setFlavor(), and the change comes back through the runtime’s event. Storage, target and media options are not used in this mode.
Standalone. In-game pages and Vite apps have no runtime. Then useFlavor loads the stored preference, follows prefers-color-scheme while the preference is 'auto', writes each change to storage, and sets data-flavor, data-theme and color-scheme on the target.
Synchronization
Section titled “Synchronization”Every instance listens on document for the static-bloom:flavor-change event, a CustomEvent whose detail is { flavor, resolved, theme }. The runtime dispatches it whenever the flavor changes, so all islands on a page, the header selector and SbFlavorSelect stay in agreement. Code outside Vue can listen to it too:
import { FLAVOR_CHANGE_EVENT } from '@static-bloom/vue'
document.addEventListener(FLAVOR_CHANGE_EVENT, (event) => { if (event instanceof CustomEvent) console.log(event.detail.flavor, event.detail.resolved)})In standalone mode useFlavor does not dispatch the event itself; it only mirrors changes that another source announces.
Lifecycle and server rendering
Section titled “Lifecycle and server rendering”useFlavortouches neitherwindownordocumentuntil the calling component mounts, so it renders on the server.- Every call inside one Vue app shares a single state, removed when the app unmounts. Each Astro island is its own app, with its own state, kept in step by the runtime event.
- Called outside a component, it uses one detached state for the whole page and starts it at once.
Related exports
Section titled “Related exports”| Export | Value or signature |
|---|---|
FLAVOR_STORAGE_KEY |
'static-bloom:flavor' |
FLAVOR_CHANGE_EVENT |
'static-bloom:flavor-change' |
flavors |
Flavor metadata, { id, label, dark }[] |
flavorNames |
['dusk', 'midnight', 'blackout', 'daybreak'] |
flavorPreferences |
'auto' followed by the flavor names |
accentNames |
The 12 accent names |
isFlavorName, isFlavorPreference, isAccentName |
Type guards for untrusted strings |
isDarkFlavor(flavor) |
true for Dusk, Midnight and Blackout |
themeOf(flavor) |
'dark' or 'light', the value of data-theme |
useToast
Section titled “useToast”Queues toasts for the SbToaster mounted in the app. Toast components and the toaster’s props are documented in Overlays & menus.
- The queue is empty.
<script setup lang="ts">import { SbButton, SbProvider, SbToaster, useToast } from '@static-bloom/vue'
const { toast, dismiss, toasts } = useToast()let count = 0
function notify(): void { count += 1 toast({ title: `Dispatch ${count}`, description: 'A new job is on the board.', intent: 'info', duration: 0, })}</script>
<template> <SbProvider> <SbToaster position="top-right" /> <div class="grid gap-3"> <div class="flex flex-wrap gap-2"> <SbButton @click="notify">Queue a toast</SbButton> <SbButton variant="outline" :disabled="toasts.length === 0" @click="dismiss(toasts[0]?.id)" >Dismiss oldest</SbButton > <SbButton variant="ghost" :disabled="toasts.length === 0" @click="dismiss()" >Dismiss all</SbButton > </div> <ul class="m-0 grid list-none gap-1 p-0 text-sm text-fg-muted"> <li v-for="item in toasts" :key="item.id"> {{ item.id }}: {{ item.title }} ({{ item.open ? 'open' : 'closing' }}) </li> <li v-if="toasts.length === 0">The queue is empty.</li> </ul> </div> </SbProvider></template>useToast() takes no arguments and returns:
| Name | Type | Description |
|---|---|---|
toast |
(options: SbToastOptions) => string |
Queues a toast and returns its id |
dismiss |
(id?: string) => void |
Closes the toast with that id, or every open toast without one |
toasts |
ComputedRef<readonly SbToastItem[]> |
The live queue, including toasts playing their exit animation (open: false) |
Toast options
Section titled “Toast options”| Option | Type | Default | Effect |
|---|---|---|---|
title |
string |
required | First line of the toast |
description |
string |
none | Second line |
intent |
'primary' | 'secondary' | 'neutral' | 'info' | 'success' | 'warning' | 'danger' |
'primary' |
Accent bar and icon color; warning and danger are announced assertively |
duration |
number |
SbProvider’s toast-duration (5000) |
Visible time in ms; 0 keeps the toast until it is dismissed |
action |
{ label: string; onClick: () => void } |
none | A button; the toast closes after onClick runs |
icon |
Component |
intent icon | Leading icon; info, success, warning and danger get one by default |
An SbToastItem is the options plus a readonly id and open. The types are exported as SbToastOptions, SbToastItem, SbToastIntent, SbToastActionOptions and UseToastReturn.
Placement and lifetime
Section titled “Placement and lifetime”- The queue is shared by the whole page: any component can call
toast(), and the toasts appear in the mountedSbToaster. Mount exactly one, insideSbProvider. SbToaster’spositionpicks the corner:'top-right','top-left','top-center','bottom-right'(default),'bottom-left'or'bottom-center'. The demo above uses'top-right'.- A closed toast leaves the queue 600 ms after it closes, once its exit animation is over.
- When the last
SbToasterunmounts, the queue is cleared, so toasts never outlive their viewport.
cn(...classes) joins class values (strings, arrays, objects) and resolves Tailwind conflicts with the Static Bloom tailwind-merge config, so a later utility replaces an earlier one, including theme utilities such as glow-sm or text-fg-muted. It is the same function as @static-bloom/tailwind/merge’s cn, documented in Class merging. Every component merges its class prop with it.
cn('rounded-md px-3 text-fg-muted', isActive && 'text-primary', 'rounded-full')// → 'px-3 text-primary rounded-full' when isActive is truetv is tailwind-variants’ tv, created with createTV({ twMergeConfig }) from @static-bloom/tailwind/merge. Recipes you build with it merge classes exactly like the components’ own recipes, so variant classes and the class you pass at call time override the base without duplicates. Recipes with tailwind-variants covers the API.
<script setup lang="ts">import { computed, ref } from 'vue'import { SbButton, SbProvider, cn, tv } from '@static-bloom/vue'
// A recipe for a project-level component, built with the package's configured `tv`.const statusChip = tv({ base: 'inline-flex items-center gap-2 rounded-md border px-3 py-1 text-sm', variants: { status: { idle: 'border-line text-fg-muted', active: 'border-success text-success', wanted: 'border-danger text-danger', }, }, defaultVariants: { status: 'idle' },})
const statuses = ['idle', 'active', 'wanted'] as constconst status = ref<(typeof statuses)[number]>('active')// `cn` lets a caller's class win over the recipe: `rounded-full` replaces `rounded-md`.const chipClass = computed(() => cn(statusChip({ status: status.value }), 'rounded-full'))</script>
<template> <SbProvider> <div class="flex flex-wrap items-center gap-3"> <span :class="chipClass">Crew status: {{ status }}</span> <SbButton v-for="item in statuses" :key="item" size="sm" :variant="status === item ? 'solid' : 'outline'" :aria-pressed="status === item" @click="status = item" > {{ item }} </SbButton> </div> </SbProvider></template>Use it to style your own components in the same idiom as the library. The package also re-exports the VariantProps and ClassValue types.
StaticBloomPlugin
Section titled “StaticBloomPlugin”A Vue plugin that registers every Sb* component globally, from every family: foundations, actions, forms, selection, date and time, color, overlays and navigation. Composables and helpers are not registered; import them.
import { createApp } from 'vue'import { StaticBloomPlugin } from '@static-bloom/vue'import App from './App.vue'
createApp(App).use(StaticBloomPlugin).mount('#app')It takes no options. Registering everything pulls every component into the bundle, so prefer named imports outside prototypes, as the overview explains. You still need SbProvider at the root of the app.