Loading…
Loading…
Allows users to select a color through various interfaces like spectrum, swatches, or input.
The Color Picker allows users to select a color value through one or more interfaces: a two-dimensional spectrum/gradient area, hue and saturation sliders, alpha (opacity) controls, predefined swatches, or direct text input for hex/RGB/HSL values.
Color pickers range from simple (a grid of preset swatches) to highly complex (full spectrum with multiple color model inputs, eyedropper tool, and color harmony suggestions). The appropriate complexity depends entirely on the user's task and expertise level.
For most applications — themes, branding, tag colors — a curated swatch palette is sufficient. Full-spectrum pickers are necessary when users need precise color control: design tools, data visualization configuration, CSS editing, or brand guideline enforcement.
The color picker is inherently visual, which creates significant accessibility challenges. Color selection by definition relies on seeing color, but the component itself must still be keyboard-navigable, screen-reader-operable, and usable by people with color vision deficiency. The text input fallback (hex/RGB values) is essential for accessibility.
When to use a Color Picker:
When NOT to use a Color Picker:
Our Color Palette Generator tool is the ideal companion for color picker implementations. It generates harmonious palettes, shows accessibility contrast ratios, and exports color values in multiple formats. Pair it with the Contrast Checker to ensure any user-selected colors meet WCAG requirements when used for text or UI elements.
| Variant | Description | Complexity | Use Case |
|---|---|---|---|
| Swatches Only | Grid of predefined color options. Click to select. | Low | Tag colors, category labels, simple theming |
| Spectrum + Hue | 2D saturation/brightness gradient with a hue slider bar. The classic "color picker." | Medium | Design tools, theme editors, CSS customization |
| Spectrum + Hue + Alpha | Full spectrum with an additional opacity/transparency slider. | Medium–High | Design tools with transparency support |
| Input Only | Text field accepting hex, RGB, or HSL values. No visual picker. | Low | Developer tools, quick entry for users who know color values |
| Compact Trigger | A small color swatch button that opens a popover with the full picker. | Varies | Form fields, settings panels, inline editors |
| Eyedropper | Uses the browser's EyeDropper API to sample a color from anywhere on screen. | Low (interaction) | Design tools, browser-native color sampling |
| Full Panel | Combines spectrum, sliders, swatches, text inputs, eyedropper, and color harmony suggestions. | High | Professional design software |
| Model | Format | Best For |
|---|---|---|
| Hex | #FF5733, #F57 | Web development — the universal web color format |
| RGB | rgb(255, 87, 51) | Web development, screen-based design |
| RGBA | rgba(255, 87, 51, 0.8) | Colors with transparency |
| HSL | hsl(14, 100%, 60%) | Intuitive for humans — hue rotation + saturation + lightness |
| HSLA | hsla(14, 100%, 60%, 0.8) | HSL with transparency |
| HSB/HSV | hsb(14, 80%, 100%) | Design tools (Figma, Photoshop use HSB internally) |
| OKLCH | oklch(70% 0.15 50) | Modern CSS — perceptually uniform lightness |
| Size | Spectrum Area | Use Case |
|---|---|---|
| Compact | 160×160px | Popovers, inline form fields |
| Default | 220×220px | Settings panels, standalone pickers |
| Large | 280×280px+ | Professional tools, dedicated color pages |
Users who work in the Color Palette Generator tool are already familiar with the spectrum + hue pattern — maintain consistency by using the same interaction model in your picker.
| Property | Type | Default | Description |
|---|---|---|---|
value | string | — | Current color value (hex string, e.g., "#FF5733") |
defaultValue | string | "#000000" | Initial color (uncontrolled) |
onChange | (color: string) => void | — | Fired on every color change (during drag, typing, swatch click) |
onChangeEnd | (color: string) => void | — | Fired when interaction ends (mouseup on spectrum, blur on input). Use for expensive operations. |
format | 'hex' | 'rgb' | 'hsl' | 'hex' | Output format of the color value |
alpha | boolean | false | Enable alpha/opacity channel |
swatches | string[] | — | Array of preset color values to display as a swatch grid |
showInput | boolean | true | Show hex/RGB/HSL text input fields |
showSpectrum | boolean | true | Show the 2D gradient spectrum |
showEyeDropper | boolean | false | Show eyedropper button (requires EyeDropper API support) |
disabled | boolean | false | Disables the picker |
label | string | — | Accessible label for the color picker |
previewSize | 'sm' | 'md' | 'lg' | 'md' | Size of the color preview swatch trigger |
popover | boolean | true | Render picker in a popover vs. inline |
returnAlpha | boolean | false | Include alpha in output (#FF573380 or rgba()) |
Most implementations provide utility methods:
toHex() → "#FF5733"toRgb() → { r: 255, g: 87, b: 51 }toHsl() → { h: 14, s: 100, l: 60 }toRgbString() → "rgb(255, 87, 51)"toHslString() → "hsl(14, 100%, 60%)"| Token Category | Token Example | Color Picker Usage |
|---|---|---|
| Color – Border | --color-neutral-300 | Input field and swatch border |
| Color – Background | --color-white | Picker panel and input background |
| Color – Text | --color-neutral-900 | Input text and labels |
| Color – Focus | --color-focus-ring | Focus indicator on inputs, swatches, and spectrum |
| Spacing – Padding | --space-3 (12px) | Panel internal padding |
| Spacing – Gap | --space-2 (8px) | Gap between swatches, between sections |
| Spacing – Swatch Size | 24px × 24px | Individual swatch clickable area |
| Border Radius | --radius-md (8px) | Panel corners, input corners |
| Border Radius – Swatch | --radius-sm (4px) or 50% | Individual swatch shape (square or circular) |
| Shadow | --shadow-lg | Popover elevation |
| Z-Index | --z-popover (40) | Popover layering |
| Cursor | crosshair | Over the spectrum gradient area |
| Transition | --duration-fast (150ms) | Swatch hover, input focus transitions |
The color picker is unique in that its primary content IS color — the spectrum gradient, hue bar, and alpha slider are rendered dynamically using CSS gradients or canvas, not design tokens. Tokens apply to the structural chrome (borders, backgrounds, spacing) around the color content.
For generating the swatch palette values to pass to the swatches prop, use the Color Palette Generator.
| State | Visual Treatment | Behavior |
|---|---|---|
| Rest (Trigger) | Color swatch showing current value, possibly with a border. Chevron or edit icon. | Click to open the picker popover. |
| Open | Popover with spectrum, sliders, swatches, and input fields visible. | User can interact with all sub-controls. |
| Spectrum Dragging | Crosshair cursor, small circle indicator follows the pointer across the 2D gradient. | Hue comes from the hue slider; saturation and brightness from X/Y position. |
| Hue Slider Dragging | Thumb moves along the rainbow gradient bar. Spectrum updates in real time. | Changes the hue of the entire spectrum. |
| Alpha Slider Dragging | Thumb on a transparency gradient (checkerboard → color). | Adjusts opacity from 0% to 100%. |
| Swatch Hover | Scale up slightly (transform: scale(1.15)) with shadow. | Indicates clickability. |
| Swatch Selected | Checkmark overlay or ring/border highlight. | Shows which preset is active. |
| Input Focus | Standard text input focus state — primary border, focus ring. | User is typing a color value directly. |
| Input Invalid | Red border, error text. | Typed value is not a valid color (e.g., "#GGHHII"). |
| Eyedropper Active | Native OS eyedropper UI (browser-controlled). | User is sampling a color from the screen. |
| Disabled | Muted trigger swatch, no interaction. | Picker cannot be opened. |
The spectrum area is a 2D gradient:
The position indicator (small circle) should:
Color pickers are inherently visual components, but they must remain operable for all users. The key principle: every color selection possible with a mouse must also be achievable via keyboard and expressible in text.
Label Association (WCAG 1.3.1, 4.1.2):
aria-label="Select background color" or an associated <label>.Keyboard Interaction (WCAG 2.1.1 Keyboard):
Enter / Space opens the popover.role="slider" or role="application" with arrow keys moving the indicator. Arrow Right increases saturation, Arrow Up increases brightness. Step size should be meaningful (e.g., 1% per press, 10% with Shift).<input type="range"> or role="slider" with aria-valuemin="0", aria-valuemax="360", aria-valuenow, and aria-valuetext="Hue: 14 degrees (red-orange)".role="slider" with aria-valuemin="0", aria-valuemax="100", aria-valuetext="Opacity: 80%".role="option" or role="radio" with aria-label="Red (#FF0000)" or similar.Escape closes the popover and returns focus to the trigger.Tab moves between sub-controls within the popover.Screen Reader Experience (WCAG 4.1.2, 4.1.3):
aria-valuetext on sliders: "Hue: 200 degrees, approximately cyan".aria-valuetext could describe: "Saturation 70%, Brightness 85%"."Selected: Ocean Blue, #0077B6"."Switched to HSL format".Color Vision Deficiency (WCAG 1.4.1):
Non-Text Contrast (WCAG 1.4.11):
1px solid var(--color-neutral-300).Target Size (WCAG 2.5.8):
Reduced Motion (WCAG 2.3.3):
prefers-reduced-motion by disabling the animation.#FFF, #FFFFFF, rgb(255,255,255), and white if possible.onChange during drag — spectrum and slider dragging fires hundreds of events per second. Throttle to ~60fps for UI updates, and use onChangeEnd for expensive operations (API calls, re-renders).When building a color picker for a design system or theming tool, consider integrating with the Color Palette Generator to:
<!-- Color Picker: Swatch + Hex Input -->
<div class="color-picker" id="color-picker">
<label class="color-picker__label" id="cp-label">Brand Color</label>
<div class="color-picker__trigger-row">
<button
type="button"
class="color-picker__swatch-btn"
id="cp-trigger"
aria-label="Select brand color. Current: #6366F1"
aria-haspopup="true"
aria-expanded="false"
style="background-color: #6366F1;"
></button>
<input
type="text"
id="cp-hex-input"
class="color-picker__hex-input"
value="#6366F1"
maxlength="7"
aria-labelledby="cp-label"
spellcheck="false"
/>
</div>
<!-- Swatches (shown inline or in popover) -->
<div class="color-picker__swatches" role="listbox" aria-label="Preset colors" id="cp-swatches">
<button role="option" aria-label="Red, #EF4444" aria-selected="false" class="color-picker__sw" style="background:#EF4444;" data-color="#EF4444"></button>
<button role="option" aria-label="Orange, #F97316" aria-selected="false" class="color-picker__sw" style="background:#F97316;" data-color="#F97316"></button>
<button role="option" aria-label="Yellow, #EAB308" aria-selected="false" class="color-picker__sw" style="background:#EAB308;" data-color="#EAB308"></button>
<button role="option" aria-label="Green, #22C55E" aria-selected="false" class="color-picker__sw" style="background:#22C55E;" data-color="#22C55E"></button>
<button role="option" aria-label="Blue, #3B82F6" aria-selected="false" class="color-picker__sw" style="background:#3B82F6;" data-color="#3B82F6"></button>
<button role="option" aria-label="Indigo, #6366F1" aria-selected="true" class="color-picker__sw color-picker__sw--selected" style="background:#6366F1;" data-color="#6366F1"></button>
<button role="option" aria-label="Purple, #A855F7" aria-selected="false" class="color-picker__sw" style="background:#A855F7;" data-color="#A855F7"></button>
<button role="option" aria-label="Pink, #EC4899" aria-selected="false" class="color-picker__sw" style="background:#EC4899;" data-color="#EC4899"></button>
</div>
<div id="cp-status" aria-live="polite" class="sr-only"></div>
</div>
<style>
.color-picker__label {
display: block;
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 0.375rem;
color: var(--color-neutral-900);
}
.color-picker__trigger-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.color-picker__swatch-btn {
width: 36px;
height: 36px;
border: 2px solid var(--color-neutral-300);
border-radius: var(--radius-md, 8px);
cursor: pointer;
flex-shrink: 0;
transition: transform 100ms ease;
}
.color-picker__swatch-btn:hover {
transform: scale(1.05);
}
.color-picker__swatch-btn:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.color-picker__hex-input {
width: 90px;
height: 36px;
padding: 0 0.5rem;
border: 1px solid var(--color-neutral-300);
border-radius: var(--radius-sm, 4px);
font-family: monospace;
font-size: 0.875rem;
color: var(--color-neutral-900);
text-transform: uppercase;
}
.color-picker__hex-input:focus {
outline: none;
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
.color-picker__swatches {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.color-picker__sw {
width: 28px;
height: 28px;
border: 2px solid transparent;
border-radius: var(--radius-sm, 4px);
cursor: pointer;
position: relative;
transition: transform 100ms ease;
}
.color-picker__sw:hover {
transform: scale(1.15);
}
.color-picker__sw:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.color-picker__sw--selected {
border-color: var(--color-neutral-900);
}
.color-picker__sw--selected::after {
content: "✓";
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>
<script>
const swatches = document.querySelectorAll(".color-picker__sw");
const hexInput = document.getElementById("cp-hex-input");
const trigger = document.getElementById("cp-trigger");
const status = document.getElementById("cp-status");
function selectColor(hex) {
trigger.style.backgroundColor = hex;
trigger.setAttribute("aria-label", "Select brand color. Current: " + hex);
hexInput.value = hex;
swatches.forEach((sw) => {
const match = sw.dataset.color === hex;
sw.classList.toggle("color-picker__sw--selected", match);
sw.setAttribute("aria-selected", String(match));
});
status.textContent = "Selected: " + hex;
}
swatches.forEach((sw) => {
sw.addEventListener("click", () => selectColor(sw.dataset.color));
});
hexInput.addEventListener("change", () => {
const val = hexInput.value.trim();
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
selectColor(val.toUpperCase());
}
});
</script>import React, { useState, useCallback, useId } from "react";
interface ColorPickerProps {
value?: string;
defaultValue?: string;
onChange?: (color: string) => void;
swatches?: string[];
label: string;
disabled?: boolean;
showInput?: boolean;
}
const DEFAULT_SWATCHES = [
"#EF4444", "#F97316", "#EAB308", "#22C55E",
"#3B82F6", "#6366F1", "#A855F7", "#EC4899",
"#14B8A6", "#64748B", "#1E293B", "#FFFFFF",
];
export function ColorPicker({
value: controlledValue,
defaultValue = "#6366F1",
onChange,
swatches = DEFAULT_SWATCHES,
label,
disabled = false,
showInput = true,
}: ColorPickerProps) {
const id = useId();
const [internal, setInternal] = useState(defaultValue);
const color = controlledValue ?? internal;
const [hexInput, setHexInput] = useState(color);
const [announcement, setAnnouncement] = useState("");
const selectColor = useCallback(
(hex: string) => {
const upper = hex.toUpperCase();
setInternal(upper);
setHexInput(upper);
onChange?.(upper);
},
[onChange]
);
const handleInputBlur = () => {
if (/^#[0-9A-Fa-f]{6}$/.test(hexInput)) {
selectColor(hexInput);
setAnnouncement(`Color set to ${hexInput.toUpperCase()}`);
} else if (/^#[0-9A-Fa-f]{3}$/.test(hexInput)) {
const expanded = "#" + hexInput[1] + hexInput[1] + hexInput[2] + hexInput[2] + hexInput[3] + hexInput[3];
selectColor(expanded);
setAnnouncement(`Color set to ${expanded}`);
} else {
setHexInput(color);
}
};
const handleSwatchClick = (hex: string) => {
selectColor(hex);
setAnnouncement(`Selected ${hex}`);
};
const isLight = (hex: string): boolean => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return (r * 299 + g * 587 + b * 114) / 1000 > 186;
};
return (
<div style={{ opacity: disabled ? 0.5 : 1, pointerEvents: disabled ? "none" : undefined }}>
<label id={`${id}-label`} style={{ display: "block", fontWeight: 600, fontSize: "0.875rem", marginBottom: 4 }}>
{label}
</label>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.75rem" }}>
<div
aria-label={`Current color: ${color}`}
style={{
width: 36,
height: 36,
borderRadius: "var(--radius-md, 8px)",
border: "2px solid var(--color-neutral-300)",
backgroundColor: color,
flexShrink: 0,
}}
/>
{showInput && (
<input
type="text"
value={hexInput}
onChange={(e) => setHexInput(e.target.value)}
onBlur={handleInputBlur}
onKeyDown={(e) => e.key === "Enter" && handleInputBlur()}
aria-labelledby={`${id}-label`}
maxLength={7}
spellCheck={false}
style={{
width: 90,
height: 36,
padding: "0 0.5rem",
border: "1px solid var(--color-neutral-300)",
borderRadius: "var(--radius-sm, 4px)",
fontFamily: "monospace",
fontSize: "0.875rem",
textTransform: "uppercase",
}}
/>
)}
</div>
{swatches.length > 0 && (
<div role="listbox" aria-label="Preset colors" style={{ display: "flex", flexWrap: "wrap", gap: "0.375rem" }}>
{swatches.map((hex) => (
<button
key={hex}
type="button"
role="option"
aria-label={`${hex}`}
aria-selected={color === hex.toUpperCase()}
onClick={() => handleSwatchClick(hex)}
style={{
width: 28,
height: 28,
backgroundColor: hex,
border: color === hex.toUpperCase() ? "2px solid var(--color-neutral-900)" : "2px solid transparent",
borderRadius: "var(--radius-sm, 4px)",
cursor: "pointer",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{color === hex.toUpperCase() && (
<span style={{ color: isLight(hex) ? "#000" : "#fff", fontSize: 14, textShadow: "0 1px 2px rgba(0,0,0,0.3)" }}>✓</span>
)}
</button>
))}
</div>
)}
<div aria-live="polite" aria-atomic="true" style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0,0,0,0)" }}>
{announcement}
</div>
</div>
);
}Material Design 3 doesn't include a dedicated color picker component in its specification. MUI provides a basic color input via <TextField type="color"> which renders the browser's native color picker. For richer implementations, the MUI ecosystem relies on third-party libraries like react-colorful or @uiw/react-color.
Ant Design provides a ColorPicker component (introduced in v5.5) with a full-featured panel: spectrum area, hue slider, alpha slider, format switcher (hex/RGB/HSB), preset swatches (presets prop with labeled groups), and a compact trigger swatch. It supports controlled/uncontrolled modes, allowClear, showText (display color value next to trigger), and trigger="hover" for opening on hover instead of click. The picker uses the HSB color model internally.
Radix UI and Headless UI do not provide color picker primitives. Color picking is considered application-level rather than a primitive UI component.
react-colorful is the most popular lightweight React color picker. At ~2 KB gzipped, it provides a spectrum + hue slider with zero dependencies. It exports components for different color models: HexColorPicker, RgbColorPicker, HslColorPicker, HsvColorPicker, and alpha variants. It's fully accessible with keyboard support on the spectrum and sliders. No swatch support — you'd build that separately.
@uiw/react-color is a comprehensive React color picker suite providing multiple picker styles: Sketch (Sketch app style), Chrome (Chrome DevTools style), Compact, Material, Wheel, Block, Github, and Slider. Each targets a different visual style and complexity level.
Figma, Sketch, and Adobe XD all use a spectrum + hue + alpha pattern with HSB/HSV as the primary color model, plus text inputs for hex and RGB. Figma adds an eyedropper and a document-colors swatch bar. These professional tools set user expectations for what a "good" color picker looks like.
Native HTML <input type="color"> provides a built-in color picker that varies by OS and browser. It's fully accessible, requires zero JavaScript, and is the simplest option — but it can't be styled, doesn't support alpha, and offers no swatches. Consider it as a progressive enhancement or fallback.
The Color Palette Generator on this site can be used alongside any color picker to generate full design token palettes from a single selected color.