Skip to content

Color

The color family wraps Reka UI’s color primitives. Every part is drawn in the page, so it replaces <input type="color">, whose browser picker does not open in OPEN//77’s in-game browser.

Values are hex strings or Reka Color objects ({ space: 'hsl', h, s, l, alpha }); v-model hands back whatever the primitive produces, so bind a ref typed with the component’s modelValue prop when you share one value between parts.

A complete picker behind one button: a Reka Popover holding a saturation and brightness area, hue and opacity sliders, a hex field, and optional presets. The trigger shows a swatch of the current color.

#59a7ff
ColorPickerDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbColorPicker, SbProvider } from '@static-bloom/vue'
const paint = ref('#59a7ff')
const presets = [
{ value: '#fd65b2', label: 'Plasma pink' },
{ value: '#b580ff', label: 'Ultraviolet' },
{ value: '#66e6ba', label: 'Coolant green' },
{ value: '#ffa73e', label: 'Sodium orange' },
{ value: '#59a7ff', label: 'Ion blue' },
]
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<SbColorPicker v-model="paint" label="Vehicle paint" :presets="presets" />
<output class="font-mono text-sm text-fg-muted">{{ paint }}</output>
</div>
</SbProvider>
</template>
Anatomy (rendered for you)
SbColorPicker
├─ trigger: SbColorSwatch + label (default slot, receives { value })
└─ popover content
├─ title + close button
├─ SbColorArea (hsb: saturation × brightness)
├─ SbColorSlider channel="hue"
├─ SbColorSlider channel="alpha"
├─ SbColorField (hex)
└─ SbColorSwatchPicker of presets (when `presets` is set)
Prop Type Default Effect
modelValue string | Color none Current color; v-model emits a string in format
defaultValue string | Color pure red Uncontrolled starting color; without it the picker starts at hsl 0, 100%, 50%
format ColorFormat 'hex' Format of the emitted string, e.g. 'hex' or 'hsl'
label string 'Choose color' Trigger text and accessible name, also the popover title
presets readonly { value; label? }[] none Swatches under the field; value is a color string
disabled boolean false Disables the trigger and every control
open boolean none Controlled open state, v-model:open
defaultOpen boolean false Uncontrolled initial open state
closeLabel string 'Close color picker' Accessible name of the close button
class HTMLAttributes['class'] none Merged onto the trigger

Emits update:modelValue (string) and update:open (boolean). Other attributes go to the trigger button.

Keyboard and accessibility. The trigger is a button named by label. Opening moves focus into the popover; Escape closes it and returns focus to the trigger. The area, sliders, field and presets work from the keyboard as described below.

A two-dimensional picker over two channels of one color space, wrapping Reka ColorAreaRoot, ColorAreaArea and ColorAreaThumb. The default slot holds the thumb; SbColorAreaThumb is rendered when the slot is empty.

Neon sign
ColorAreaDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbColorArea, SbColorSwatch, SbProvider, type SbColorAreaProps } from '@static-bloom/vue'
const glow = ref<SbColorAreaProps['modelValue']>('#fd65b2')
</script>
<template>
<SbProvider>
<div class="grid w-full max-w-sm gap-3">
<SbColorArea
v-model="glow"
color-space="hsb"
x-channel="saturation"
y-channel="brightness"
aria-label="Neon sign saturation and brightness"
/>
<div class="flex items-center gap-3 text-sm text-fg-muted">
<SbColorSwatch :color="glow" label="Neon sign color" />
Neon sign
</div>
</div>
</SbProvider>
</template>
Anatomy
SbColorArea
└─ SbColorAreaThumb (default slot content)
Prop Type Default Effect
modelValue string | Color none Controlled color, v-model
defaultValue string | Color '#ff0000' Uncontrolled starting color (Reka)
colorSpace ColorSpace 'hsl' Color space the channels belong to (Reka)
xChannel ColorChannel 'hue' Channel on the horizontal axis (Reka)
yChannel ColorChannel 'saturation' Channel on the vertical axis (Reka)
disabled boolean false Blocks interaction and dims the area
xName, yName string none Input names for form submission (Reka)
class HTMLAttributes['class'] none Merged onto the root

The other Reka ColorAreaRoot props and emits pass through.

Keyboard. The thumb is the focusable control: arrow keys move it along both axes. Give the area an aria-label that names both channels.

One channel of a color on a gradient track, wrapping Reka ColorSliderRoot and ColorSliderThumb. The track paints the channel’s gradient over a checkerboard, so alpha is visible.

Jacket dye
Hue
Saturation
Lightness
Opacity
ColorSliderDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import {
SbColorSlider,
SbColorSwatch,
SbProvider,
type SbColorSliderProps,
} from '@static-bloom/vue'
const dye = ref<SbColorSliderProps['modelValue']>('#b580ffcc')
const channels = [
{ channel: 'hue', label: 'Hue' },
{ channel: 'saturation', label: 'Saturation' },
{ channel: 'lightness', label: 'Lightness' },
{ channel: 'alpha', label: 'Opacity' },
] as const
</script>
<template>
<SbProvider>
<div class="grid w-full max-w-sm gap-2">
<div class="flex items-center gap-3 text-sm text-fg-muted">
<SbColorSwatch :color="dye" size="lg" label="Jacket dye" />
Jacket dye
</div>
<div v-for="item in channels" :key="item.channel" class="grid gap-1 text-xs text-fg-muted">
<span>{{ item.label }}</span>
<SbColorSlider v-model="dye" :channel="item.channel" :label="item.label" />
</div>
</div>
</SbProvider>
</template>
Prop Type Default Effect
channel ColorChannel required 'hue', 'saturation', 'lightness', 'alpha', …
modelValue string | Color none Controlled color, v-model
defaultValue string | Color '#000000' Uncontrolled starting color (Reka)
colorSpace ColorSpace 'hsl' Color space of channel (Reka)
orientation 'horizontal' | 'vertical' 'horizontal' Vertical sliders are 12rem tall; the gradient follows
inverted boolean false Reverses the axis (Reka)
step number channel step Keyboard step (Reka)
label string none aria-label of the thumb, the focusable control
disabled boolean false Blocks interaction and dims the slider
class HTMLAttributes['class'] none Merged onto the root

The other Reka ColorSliderRoot props and emits pass through. SbColorSliderTrack is exported for custom slider layouts.

Keyboard. Arrow keys change the channel by step; the thumb exposes the slider role with its orientation. Set label so it has a name.

A text input for a color, wrapping Reka ColorFieldRoot and ColorFieldInput. Without channel it edits the hex value; with channel it edits one number.

ColorFieldDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbColorField, SbColorSwatch, SbProvider, type SbColorFieldProps } from '@static-bloom/vue'
const trim = ref<SbColorFieldProps['modelValue']>('#66e6ba')
</script>
<template>
<SbProvider>
<div class="grid w-full max-w-xs gap-4">
<label class="grid gap-2 text-sm text-fg-muted">
Trim color
<div class="flex items-center gap-3">
<SbColorSwatch :color="trim" label="Trim color" />
<SbColorField v-model="trim" label="Trim color" />
</div>
</label>
<label class="grid gap-2 text-sm text-fg-muted">
Trim hue
<SbColorField v-model="trim" channel="hue" size="sm" label="Trim hue" />
</label>
</div>
</SbProvider>
</template>
Prop Type Default Effect
modelValue string | Color none Controlled color, v-model
channel ColorChannel none Edit one channel instead of the hex value (Reka)
colorSpace ColorSpace 'hsl' Space of channel (Reka)
size 'sm' | 'md' | 'lg' 'md' Input height 2rem, 2.5rem or 3rem
label string 'Color value' aria-label of the input
invalid boolean false Sets aria-invalid and the danger border
disabled boolean false Disables the input
readonly boolean false Read-only input (Reka)
placeholder string none Placeholder when empty (Reka)
class HTMLAttributes['class'] none Merged onto the input

The other Reka ColorFieldRoot props and emits pass through; other attributes (such as id or aria-describedby) go to the input.

Keyboard and accessibility. The value is parsed when the input commits: invalid text is rejected and the previous color is kept; channel values are clamped. Arrow keys step the value. Point an error message at the input with aria-describedby and set invalid.

A display-only preview of one color over a checkerboard, wrapping Reka ColorSwatch. Reka gives it role="img" and a name derived from the color unless you pass label.

ColorSwatchDemo.vue
<script setup lang="ts">
import { SbColorSwatch, SbProvider } from '@static-bloom/vue'
const sizes = ['sm', 'md', 'lg'] as const
</script>
<template>
<SbProvider>
<div class="flex flex-wrap items-center gap-4">
<SbColorSwatch
v-for="size in sizes"
:key="size"
color="#ffa73e"
:size="size"
:label="`Sodium orange, ${size}`"
/>
<SbColorSwatch color="#ffa73e" size="lg" shape="square" label="Sodium orange, square" />
<SbColorSwatch color="#59a7ff80" size="lg" label="Ion blue at half opacity" />
</div>
</SbProvider>
</template>
Prop Type Default Effect
color string | Color none Color to show (Reka)
label string color name Accessible name (Reka)
size 'sm' | 'md' | 'lg' 'md' 1.5rem, 2rem or 2.75rem square
shape 'rounded' | 'square' 'rounded' Corner radius
class HTMLAttributes['class'] none Merged onto the root

A grid of preset swatches, one of which is selected, wrapping Reka ColorSwatchPickerRoot (a listbox) and ColorSwatchPickerItem. Each item is 2.75rem square and shows a check when selected.

#66e6ba
ColorSwatchPickerDemo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { SbColorSwatchPicker, SbColorSwatchPickerItem, SbProvider } from '@static-bloom/vue'
const crewColor = ref('#66e6ba')
const presets = [
{ value: '#fd65b2', label: 'Plasma pink' },
{ value: '#b580ff', label: 'Ultraviolet' },
{ value: '#66e6ba', label: 'Coolant green' },
{ value: '#ffa73e', label: 'Sodium orange' },
{ value: '#59a7ff', label: 'Ion blue' },
]
</script>
<template>
<SbProvider>
<div class="grid w-full max-w-xs gap-3">
<SbColorSwatchPicker v-model="crewColor" aria-label="Crew color">
<SbColorSwatchPickerItem
v-for="preset in presets"
:key="preset.value"
:value="preset.value"
:label="preset.label"
/>
</SbColorSwatchPicker>
<output class="font-mono text-sm text-fg-muted">{{ crewColor }}</output>
</div>
</SbProvider>
</template>
Anatomy
SbColorSwatchPicker
└─ SbColorSwatchPickerItem (one per preset)
Part Prop Type Default Effect
SbColorSwatchPicker modelValue string | string[] none Selected color, v-model
defaultValue string | string[] none Uncontrolled selection
disabled boolean false Disables every item
class HTMLAttributes['class'] none Merged onto the grid
SbColorSwatchPickerItem value string required The color, a hex string
label string none Accessible name of the option and its swatch
shape 'rounded' | 'square' 'rounded' Swatch shape

The picker accepts the other Reka listbox props (multiple, orientation, …) except by. The default slot of an item receives { selected } to replace the swatch.

Keyboard and accessibility. Give the picker an aria-label. Arrow keys move between swatches and select them, updating v-model.

  • Every popup and control here is DOM-rendered, so the picker works in OPEN//77’s in-game browser, where native color inputs do not open.
  • The picker’s popover portals to <body> (or SbProvider’s teleport-to). On a scoped island, point teleport-to inside the scope to keep the popover in its flavor.
  • The parts are sized for cursor and controller input: the trigger and swatch items are at least 2.75rem (44 px), sliders are 2.75rem tall, and thumbs extend their hit area 0.5rem beyond the visible ring.