Styleframe Logo
Forms

Color Picker

A color selection control composed of a saturation/value selector square, a vertical hue track, and shared draggable thumbs. Renders any color through a CSS variable and gradients, with five sizes through the recipe system.

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--hue variable.
  • 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 xs to xl keep 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:

src/components/color-picker.styleframe.ts
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:

src/components/ColorPicker.tsx
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 thumbleft is the saturation (0–100%), top is the inverse of value/brightness (0% = full brightness at the top).
  • Track thumbtop is the hue as a fraction of 360 (e.g. hue 18050%).

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.

Good to know: The selector's black/white gradient overlays and the track's rainbow are fixed color-model constants, so they are intentionally literal rather than themeable tokens — the math only works with real black, white, and a full-saturation hue.

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

SizeSelector (square)Track heightTrack 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:

PartRecipeRole
RootuseColorPickerRecipe()The .color-picker flex container — lays out the selector and track and owns the [data-disabled] state
SelectoruseColorPickerSelectorRecipe()The .color-picker-selector HSV square — saturation/value gradient driven by --color-picker--hue
TrackuseColorPickerTrackRecipe()The .color-picker-track hue slider — the fixed 0–360° rainbow
ThumbuseColorPickerThumbRecipe()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>
Pro tip: The thumb recipe is intentionally generic — the same .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 an aria-label (or aria-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-thumb aria-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:

src/components/color-picker.styleframe.ts
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:

src/components/color-picker.styleframe.ts
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;
Good to know: Filtering also adjusts default variants that reference filtered-out values, so your recipe stays consistent.

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:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the container
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
sizexs, sm, md, lg, xlmd

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:

VariantOptionsDefault
sizexs, sm, md, lg, xlmd

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:

VariantOptionsDefault
sizexs, sm, md, lg, xlmd

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.

Learn more about recipes →

Best Practices

  • Drive --color-picker--hue from 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 / top from your hue, saturation, and value — the recipe leaves positioning to you on purpose.
  • Pass size consistently: Spread the same size to 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