oriUI

Color picker

An accessible color picker: a 2D saturation × brightness area, a hue slider, a hex field, and optional preset swatches — an inline panel you compose into an OriPopover for a swatch-triggered flow. Behaviour lives in the framework-agnostic useColorPicker (a zero-dependency sRGB + 2D-area engine, no adapter or machine); the styled component reuses OriSlider for the hue channel and OriInput for the hex field. v-model carries a lowercase color string; the internal working model is HSVA, so the hue survives when you drag into a corner and back.

The 2D area is built from two visually-hidden native <input type="range"> — one per axis — so the browser supplies role="slider", focus, and value announcements; the area's keydown routes the arrows in 2D. See Accessibility.

Classes

The panel is a block wrapper plus BEM parts. The swatch and each preset read an inline --ori-color (an arbitrary color, not a ori-color_* role token); the area reads an inline --ori-hue.

ClassTypeDescription
ori-color-pickerBlockRequired base class (wrapper). Fixed width via --ori-color-picker-size (15rem).
ori-color-picker__areaPartThe 2D saturation×brightness surface; reads the inline --ori-hue gradient base.
ori-color-picker__area-thumbPartThe area indicator, positioned by inline left / top.
ori-color-picker__channelPartThe two visually-hidden (saturation, brightness) — the a11y surface.
ori-color-picker__controlsPartFlex row: the preview swatch, the hue (and optional alpha) slider, and the optional eyedropper button.
ori-color-picker__slidersPartColumn holding the hue slider and, when alpha is set, the alpha slider.
ori-color-picker__swatchPartCurrent-color preview; reads inline --ori-color (background) and --ori-ink (readable content color).
ori-color-picker__swatch_alphaModifierOn the swatch when alpha is set — adds the transparency checkerboard behind the (semi-transparent) current color.
ori-color-picker__hexPartThe hex OriInput field.
ori-color-picker__eyedropperPartThe eyedropper OriButton — shown only when eyedropper is set and the browser supports the EyeDropper API.
ori-color-picker__presetsPartA single-select roving listbox of preset swatches.
ori-color-picker__presetPartA preset chip (button); reads inline --ori-color. Selected via aria-selected.
ori-slider_hueModifierOn the hue OriSlider — repaints the track as the full spectrum.
ori-slider_alphaModifierOn the alpha OriSlider — a transparent→color gradient over a checkerboard (transparency grid).
data-disabledStateSet on the wrapper when disabled; dims the panel and blocks pointer events.

Live demo

Drag inside the area, move the hue slider, type a hex, or pick a preset. Each control drives the same color; v-model is the lowercase hex string.

vue
<script setup>
import { ref } from 'vue';
const color = ref('#3366ff');
const palette = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#111827'];
</script>

<template>
    <OriColorPicker v-model="color" label="Brand color" :presets="palette" />
</template>

In a popover (swatch trigger)

The panel is open-state-agnostic — drop it inside OriPopover and reuse its #trigger. This is the fill/stroke-color pattern for a toolbar.

vue
<script setup>
import { ref } from 'vue';
const color = ref('#3366ff');
</script>

<template>
    <OriPopover>
        <template #trigger>
            <button
                type="button"
                aria-label="Stroke color"
                :style="{ background: color, width: '1.5rem', height: '1.5rem', borderRadius: '50%' }"
            />
        </template>
        <OriColorPicker v-model="color" label="Stroke color" />
    </OriPopover>
</template>

Output format

format selects the emitted string; the default is hex. rgb and hsl emit function notation. All output is lowercase (a common validator constraint).

vue
<OriColorPicker v-model="a" format="hex" />
<OriColorPicker v-model="b" format="rgb" />
<OriColorPicker v-model="c" format="hsl" />

Alpha (transparency)

alpha adds an alpha channel — a checkerboard slider and a swatch that shows the transparency, plus #rrggbbaa output (or rgba() / hsla() per format). The picker also parses 8-digit hex / rgba() input, so v-model round-trips transparency.

vue
<!-- color round-trips as e.g. "#3366ff80" -->
<OriColorPicker v-model="color" :alpha="true" label="Fill color" />

Eyedropper

eyedropper shows a pick-from-screen trigger built on the EyeDropper API. It is feature-detected — the trigger is hidden where the browser lacks it (currently Chromium-only), so it is never a dead button. A picked color keeps the current alpha.

vue
<OriColorPicker v-model="color" :eyedropper="true" label="Brand color" />

Commit on release

Like OriSlider, update:modelValue streams the color live on every interaction tick (drag, arrow, hue move), while change fires once on release / keyboard settle — so a whole drag records a single undo entry. Bind v-model for the live value and @change for the commit.

vue
<template>
    <!-- v-model tracks the live color; @change commits the settled value (one undo entry) -->
    <OriColorPicker v-model="color" label="Fill" @change="pushUndo" />
</template>

Form submission

Set name to submit the picker in a native form: it renders a hidden input carrying the current color, so the picker joins form submission like <input type="color">. Unlike a combobox, a color control has no empty state — it always submits a color (its current one, default black), even before the user interacts. The submitted string uses the emitted format; a disabled picker is excluded from submission (matching a native disabled control). Pass form to associate the hidden input with a <form> by id when the picker sits outside it.

vue
<template>
    <form>
        <OriColorPicker v-model="color" name="brand" label="Brand color" />
    </form>
</template>

Accessibility

  • The 2D area is two visually-hidden native <input type="range"> (saturation, brightness), each a real role="slider" with its own aria-label and aria-valuetext ("60%") — focusable, announced, and form-associable, with no hand-rolled slider ARIA. The area's keydown routes the arrow keys across the two axes (a single native range can't span both).
  • The hue slider is an OriSlider (a native range → role="slider" + the full keyboard contract). The hex field is an OriInput — a bad or partial hex flips it to aria-invalid and is rejected on commit.
  • The preview swatch is role="img" named by the current color; presets are a single-select role="listbox" with roving tabindex (one tab stop; aria-selected marks the active color).
  • label names the whole control (role="group"). disabled dims the panel, blocks pointer events, and disables the channel + hex inputs.

Area keyboard (focus is on a channel input):

KeyAction
ArrowLeft / ArrowRightDecrease / increase saturation by 1% (Shift = 10%).
ArrowDown / ArrowUpDecrease / increase brightness by 1% (Shift = 10%).
PageDown / PageUpDecrease / increase brightness by 10%.
Home / EndJump saturation to 0% / 100%.

Framework API

The props, events, and slots of the Vue component. Behaviour comes from useColorPicker (@oriui/headless/vue); the standalone CSS layer renders the panel statically from the classes (interactivity is JS). (Svelte bindings are planned.)

Props

PropTypeDefaultDescription
alphabooleanAdd an alpha channel: a checkerboard slider + #rrggbbaa (or rgba() / hsla()) output.
disabledbooleanDims the panel, blocks pointer events, and disables the channel + hex inputs.
eyedropperbooleanShow a pick-from-screen trigger (EyeDropper API; auto-hidden where the browser lacks it).
formstringAssociate the hidden value input with a <form> by id (when the picker sits outside it).
format'hex' | 'rgb' | 'hsl''hex'Output format of the emitted string. Always lowercase.
labelstringAccessible name for the whole control (→ aria-label on the role="group" root).
modelValuestringControlled color; bind with v-model. Parsed loosely (hex, rgb()/rgba(), hsl()/hsla()).
namestringSubmit the current color under this field name via a hidden input (a color always has a value).
presetsstring[]Preset swatch colors, rendered as a single-select roving listbox.

Events

EventPayloadDescription
update:modelValuestringFired live on every interaction tick (drag / arrow / hue / hex commit / preset); drives v-model.
changestringFired once when the color is committed (pointer release / keyboard settle) — one undo entry.

Slots

SlotPropsDescription
#swatchOverlay content for the preview swatch (e.g. an icon). The swatch color/ink render behind it.
#preset{ preset, index, selected }Per-preset content (the singular of presets). Fallback: the chip shows its color.

See also