Color Picker
Overview
The Color Picker is a control for choosing a color visually. It is composed of four recipe parts:
useColorPickerRecipe()— the root container that lays the saturation/value selector next to the vertical hue track and owns the[data-disabled]state.useColorPickerSelectorRecipe()— the square saturation/value (HSV) area, painted with a gradient driven by the--color-picker--huevariable.useColorPickerTrackRecipe()— the narrow vertical hue slider, painted with the full 0–360° hue rainbow.useColorPickerThumbRecipe()— the draggable handle, shared by both the selector and the track.
Unlike most recipes, the Color Picker has no color axis — it renders arbitrary user colors through CSS gradients and a hue variable, not theme tokens. The recipe is the styling shell: it draws the surfaces and the handles, while dragging, color math, and format conversion stay in your application's JavaScript (or a headless library).
The Color Picker recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.
Why use the Color Picker recipe?
The Color Picker recipe helps you:
- Ship the hard CSS for free: The HSV gradient, the hue rainbow, the aligned square/track, and the contrast-ringed thumbs are all handled — you wire up the interaction.
- Render any color with one variable: Set
--color-picker--hue(0–360) and the selector gradient follows; position the thumbs to reflect the current selection. - Scale consistently: Five sizes from
xstoxlkeep the selector and track in proportion, with a fixed-size thumb that stays easy to grab. - Customize without forking: Override base styles, default variants, or filter out sizes you don't need — all through the options API.
- Stay type-safe: Full TypeScript support means your editor catches invalid size values at compile time.
Usage
Register the recipes
Add the Color Picker recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import {
useColorPickerRecipe,
useColorPickerSelectorRecipe,
useColorPickerTrackRecipe,
useColorPickerThumbRecipe,
} from '@styleframe/theme';
const s = styleframe();
const colorPicker = useColorPickerRecipe(s);
const colorPickerSelector = useColorPickerSelectorRecipe(s);
const colorPickerTrack = useColorPickerTrackRecipe(s);
const colorPickerThumb = useColorPickerThumbRecipe(s);
export default s;
Build the component
Import the runtime functions from the virtual module. Set the current hue on the root via the --color-picker--hue custom property, and position each thumb to reflect the selection — those two inputs are runtime data, so they are applied inline and driven by your drag handlers:
import {
colorPicker,
colorPickerSelector,
colorPickerTrack,
colorPickerThumb,
} from "virtual:styleframe";
interface ColorPickerProps {
size?: "xs" | "sm" | "md" | "lg" | "xl";
hue?: number; // 0–360
saturation?: number; // 0–1
value?: number; // 0–1
disabled?: boolean;
}
export function ColorPicker({
size = "md",
hue = 0,
saturation = 1,
value = 1,
disabled = false,
}: ColorPickerProps) {
return (
<div
className={colorPicker({ size })}
data-disabled={disabled ? "" : undefined}
style={{ "--color-picker--hue": hue } as React.CSSProperties}
role="group"
aria-label="Color picker"
>
<div className={colorPickerSelector({ size })}>
<div
className={colorPickerThumb()}
style={{ left: `${saturation * 100}%`, top: `${(1 - value) * 100}%`, transform: "translate(-50%, -50%)" }}
/>
</div>
<div className={colorPickerTrack({ size })}>
<div
className={colorPickerThumb()}
style={{ left: "50%", top: `${(hue / 360) * 100}%`, transform: "translate(-50%, -50%)" }}
/>
</div>
</div>
);
}
See it in action
Selecting a color
The picker exposes a single public input: the --color-picker--hue custom property (0–360, default 0). Set it on the root — it cascades to the selector, whose gradient repaints to the chosen hue. The hue track itself is a fixed rainbow, so it never changes.
The thumb positions are the other half of the state, and they belong to you:
- Selector thumb —
leftis the saturation (0–100%),topis the inverse of value/brightness (0% = full brightness at the top). - Track thumb —
topis the hue as a fraction of 360 (e.g. hue180→50%).
Because hue, saturation, and value are live data, they are applied inline and updated by your drag handlers — the recipe deliberately leaves positioning to the consumer. Pair it with your own pointer logic or a headless color library to compute the values.
Sizes
Five size variants from xs to xl scale the selector square, the hue track (both its height and its pill rail thickness), and the container gap together. The thumb stays a constant size so the handle is always comfortable to grab. Pass the same size to the root, selector, and track.
Size Reference
| Size | Selector (square) | Track height | Track width (rail) | Thumb |
|---|---|---|---|---|
xs | @9.5 (152px) | @9.5 | @0.25 (4px) | @1 (16px) |
sm | @10 (160px) | @10 | @0.375 (6px) | @1 |
md | @10.5 (168px) | @10.5 | @0.5 (8px) | @1 |
lg | @11 (176px) | @11 | @0.75 (12px) | @1 |
xl | @11.5 (184px) | @11.5 | @1 (16px) | @1 |
Disabled
Signal a disabled picker with the data-disabled attribute on the root (the reka-ui convention). The whole control dims and every thumb shows a not-allowed cursor. The recipe styles the disabled appearance only — block the pointer interaction in your own handlers.
Anatomy
The Color Picker is composed of four independent recipes. The root lays out the selector and the track; a thumb is dropped into each:
| Part | Recipe | Role |
|---|---|---|
| Root | useColorPickerRecipe() | The .color-picker flex container — lays out the selector and track and owns the [data-disabled] state |
| Selector | useColorPickerSelectorRecipe() | The .color-picker-selector HSV square — saturation/value gradient driven by --color-picker--hue |
| Track | useColorPickerTrackRecipe() | The .color-picker-track hue slider — the fixed 0–360° rainbow |
| Thumb | useColorPickerThumbRecipe() | The .color-picker-thumb handle — a fixed-size circle with a white contrast ring; used in both the selector and the track |
<div class="colorPicker(...)" role="group" aria-label="Color picker" style="--color-picker--hue: 217">
<!-- Saturation / value square -->
<div class="colorPickerSelector(...)">
<div class="colorPickerThumb(...)" style="left: 80%; top: 20%"></div>
</div>
<!-- Hue track -->
<div class="colorPickerTrack(...)">
<div class="colorPickerThumb(...)" style="top: 60%"></div>
</div>
</div>
.color-picker-thumb is reused for both handles. Position it with left / top and a translate(-50%, -50%) to center it on the selection point.Accessibility
- Group and label the control. Give the root
role="group"and anaria-label(oraria-labelledby) so assistive technology announces it as a color picker. The recipe styles the surfaces but does not manage the interaction — pair it with your own pointer logic or a headless library. - Expose the value in text. Visual gradients are invisible to screen readers; render the selected color as text (hex or rgb) alongside the picker, or mirror it into a native
<input type="color">for keyboard and assistive access. - Keep the thumbs decorative. Mark each
.color-picker-thumbaria-hidden="true"— the meaningful value lives in your text field, not the handle. - Support keyboard input. A pure drag surface is not keyboard-accessible on its own; provide arrow-key handling on a focusable element or a paired text input so the color can be set without a pointer.
- Reflect the disabled state. When you set
data-disabled, also disable the paired inputs and skip your pointer handlers so the dimmed appearance matches real behavior.
Customization
Overriding Defaults
Each color-picker composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify what you want to change:
import { styleframe } from 'virtual:styleframe';
import { useColorPickerSelectorRecipe } from '@styleframe/theme';
const s = styleframe();
const colorPickerSelector = useColorPickerSelectorRecipe(s, {
base: {
borderRadius: '@border-radius.lg',
},
defaultVariants: {
size: 'lg',
},
});
export default s;
Filtering Variants
If you only need a subset of the available sizes, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { useColorPickerRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate the md and lg sizes
const colorPicker = useColorPickerRecipe(s, {
filter: {
size: ['md', 'lg'],
},
});
export default s;
API Reference
useColorPickerRecipe(s, options?)
Creates the root recipe — the .color-picker flex container. Registers nested .color-picker[data-disabled] selectors that dim the control and mark thumbs as not-allowed.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the container |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
size | xs, sm, md, lg, xl | md |
useColorPickerSelectorRecipe(s, options?)
Creates the selector recipe — the .color-picker-selector saturation/value square. Registers the color-picker.hue variable (--color-picker--hue, default 0) and paints the HSV gradient. Accepts the same parameters as useColorPickerRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
size | xs, sm, md, lg, xl | md |
useColorPickerTrackRecipe(s, options?)
Creates the track recipe — the .color-picker-track vertical hue slider, a pill-shaped rail modeled on the Slider track and filled with the rainbow. The size axis scales both the rail thickness (width) and the height to match the selector. Paints the fixed 0–360° hue rainbow. Accepts the same parameters as useColorPickerRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
size | xs, sm, md, lg, xl | md |
useColorPickerThumbRecipe(s, options?)
Creates the thumb recipe — the .color-picker-thumb handle shared by both the selector and the track. A fixed-size circle with a white contrast ring; has no variant axis. Registers a .color-picker-thumb[data-disabled] selector for the disabled cursor. Accepts the same parameters as useColorPickerRecipe.
This recipe has no variants — the thumb is a constant size across all picker sizes.
Best Practices
- Drive
--color-picker--huefrom your state: Set the hue variable on the root and let the selector gradient follow; never hard-code the gradient yourself. - Own the thumb positions: Compute
left/topfrom your hue, saturation, and value — the recipe leaves positioning to you on purpose. - Pass
sizeconsistently: Spread the samesizeto the root, selector, and track so the control scales as one. - Mirror the value into text or a native input: Expose the selected color as text or an
<input type="color">for accessibility and keyboard support. - Pair with interaction logic: The recipe is CSS only — add your own pointer handlers or a headless color library for dragging and color conversion.
FAQ
--color-picker--hue and the thumb positions.color axis, it exposes the --color-picker--hue variable and gradient-painted surfaces. The only static axis is size.--color-picker--hue on the root and position the two thumbs: the selector thumb by saturation (left) and value (top), the track thumb by hue (top). See Selecting a color.useColorPickerTrackRecipe() element with your own checkerboard-plus-alpha gradient and a shared .color-picker-thumb.Checkbox Group
A layout component for arranging a set of checkboxes with shared orientation and spacing. Supports vertical and horizontal layouts and three gap sizes through the recipe system.
Field Group
A layout component for joining buttons, inputs, selects, and other bordered controls into a single unit with merged borders and shared orientation. Supports horizontal and vertical layouts plus full-width block mode through the recipe system.