Package Exports
- @markoradak/color-picker
- @markoradak/color-picker/presets
- @markoradak/color-picker/styles
- @markoradak/color-picker/styles.css
Readme
@markoradak/color-picker
A compound-component React color picker and gradient editor. Built with Radix primitives, fully accessible, and themeable via CSS custom properties or Tailwind CSS.
Installation
pnpm add @markoradak/color-picker
# or
npm install @markoradak/color-picker
# or
yarn add @markoradak/color-pickerPeer dependencies: react >= 18.0.0 and react-dom >= 18.0.0
Quick Start
Pre-composed Popover (easiest)
A ready-to-use popover picker with all controls included:
import { useState } from "react";
import { ColorPickerPopover } from "@markoradak/color-picker/presets";
import "@markoradak/color-picker/styles";
function App() {
const [color, setColor] = useState("#3b82f6");
return (
<ColorPickerPopover
value={color}
onValueChange={setColor}
swatches={["#ef4444", "#22c55e", "#3b82f6"]}
/>
);
}Pre-composed Inline
An always-visible picker (no popover):
import { useState } from "react";
import { ColorPickerInline } from "@markoradak/color-picker/presets";
import "@markoradak/color-picker/styles";
function App() {
const [color, setColor] = useState("#3b82f6");
return (
<ColorPickerInline value={color} onValueChange={setColor} />
);
}Compound Components (full control)
Build your own layout by composing individual primitives. Both dot notation (ColorPicker.Area) and named imports (ColorPickerArea) work:
import { useState } from "react";
import { ColorPicker } from "@markoradak/color-picker";
import "@markoradak/color-picker/styles";
function MyColorPicker() {
const [color, setColor] = useState("#3b82f6");
return (
<ColorPicker value={color} onValueChange={setColor}>
<ColorPicker.Trigger />
<ColorPicker.Content>
<ColorPicker.Area>
<ColorPicker.AreaGradient />
<ColorPicker.AreaThumb />
</ColorPicker.Area>
<ColorPicker.HueSlider>
<ColorPicker.HueSliderTrack />
<ColorPicker.HueSliderThumb />
</ColorPicker.HueSlider>
<ColorPicker.AlphaSlider>
<ColorPicker.AlphaSliderTrack />
<ColorPicker.AlphaSliderThumb />
</ColorPicker.AlphaSlider>
<ColorPicker.Input />
<ColorPicker.EyeDropper />
<ColorPicker.Swatches
values={["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6"]}
/>
</ColorPicker.Content>
</ColorPicker>
);
}Or using named imports:
import {
ColorPicker,
ColorPickerTrigger,
ColorPickerContent,
ColorPickerArea,
ColorPickerAreaGradient,
ColorPickerAreaThumb,
ColorPickerHueSlider,
ColorPickerHueSliderTrack,
ColorPickerHueSliderThumb,
ColorPickerInput,
ColorPickerEyeDropper,
ColorPickerSwatches,
ColorPickerSwatch,
} from "@markoradak/color-picker";Gradient Picker
Pass a GradientValue object instead of a string to enable gradient editing:
import { useState } from "react";
import { ColorPicker, createDefaultGradient } from "@markoradak/color-picker";
import type { ColorPickerValue } from "@markoradak/color-picker";
import "@markoradak/color-picker/styles";
function GradientPicker() {
const [value, setValue] = useState<ColorPickerValue>(
createDefaultGradient("linear")
);
return (
<ColorPicker value={value} onValueChange={setValue}>
<ColorPicker.Trigger />
<ColorPicker.Content>
<ColorPicker.ModeSelector>
{(["solid", "linear", "radial", "conic", "mesh"] as const).map(
(mode) => (
<ColorPicker.ModeSelectorItem key={mode} value={mode} />
)
)}
</ColorPicker.ModeSelector>
<ColorPicker.GradientEditor />
<ColorPicker.Area>
<ColorPicker.AreaGradient />
<ColorPicker.AreaThumb />
</ColorPicker.Area>
<ColorPicker.HueSlider>
<ColorPicker.HueSliderTrack />
<ColorPicker.HueSliderThumb />
</ColorPicker.HueSlider>
<ColorPicker.Input />
</ColorPicker.Content>
</ColorPicker>
);
}Supported gradient types: linear, radial, conic, and mesh.
Color Tokens
The picker supports named color tokens. Tokens can be provided manually or auto-detected from CSS custom properties:
// Manual tokens
<ColorPicker
value="brand-primary"
onValueChange={setValue}
tokens={{
"brand-primary": "#3b82f6",
"brand-secondary": "#8b5cf6",
danger: "#ef4444",
}}
/>
// Auto-detect CSS custom properties from :root / html
<ColorPicker value={color} onValueChange={setColor} autoTokens />
// Auto-detect with prefix filter (strips prefix from display names)
<ColorPicker value={color} onValueChange={setColor} autoTokens={{ prefix: "--brand-" }} />
// Disable auto-detection (enabled by default)
<ColorPicker value={color} onValueChange={setColor} autoTokens={false} />When tokens are available, the input field shows a token badge. Clicking it opens a searchable dropdown listing all tokens.
Input Trigger
Use ColorPickerInputTrigger (or triggerMode="input" on presets) for an input-style trigger that shows the color value inline:
<ColorPicker value={color} onValueChange={setColor}>
<ColorPickerInputTrigger />
<ColorPicker.Content>
{/* ... controls ... */}
</ColorPicker.Content>
</ColorPicker>Contrast Checking (WCAG)
Both presets render a contrast-info row and a threshold line across the color area when a contrastColor is passed:
<ColorPickerPopover
value={color}
onValueChange={setColor}
contrastColor="#ffffff"
onContrastColorChange={setBg}
/>For the composable API, place ColorPickerContrastInfo where you want the ratio readout and ColorPickerContrastLine inside ColorPickerArea between the gradient and the thumb:
<ColorPicker value={color} onValueChange={setColor}>
<ColorPickerContrastInfo contrastColor="#ffffff" onContrastColorChange={setBg} />
<ColorPickerArea>
<ColorPickerAreaGradient />
<ColorPickerContrastLine contrastColor="#ffffff" />
<ColorPickerAreaThumb />
</ColorPickerArea>
{/* ... */}
</ColorPicker>ColorPickerContrastInfo shows the current ratio (e.g. 5.23 : 1) and a WCAG level badge (AAA, AA, AA18, or Insufficient). ColorPickerContrastLine renders an SVG curve along the 4.5:1 threshold inside the saturation/brightness area, with a dot pattern in the failing region. Pass threshold={3} to target AA Large.
ColorPickerProvider (advanced)
ColorPickerProvider is a context-only wrapper (no Radix Popover.Root) that manages a single solid color. Use it when you need to embed color-picker controls inside another popover — for example, a per-stop color editor inside the gradient editor's own popover. For standard usage, prefer ColorPicker.
Styling
The library ships unstyled primitives. There are three ways to style them:
1. CSS Custom Properties Theme (recommended for most apps)
Import the included stylesheet for a complete, themeable look:
import "@markoradak/color-picker/styles";Override CSS custom properties to match your design system:
.my-theme {
--cp-bg: #1a1a2e;
--cp-border: #2a2a4a;
--cp-border-focus: #60a5fa;
--cp-text: #f5f5f5;
--cp-radius: 12px;
--cp-width: 280px;
}2. Tailwind CSS Classes
Pass Tailwind classes via className and classNames props:
<ColorPickerArea className="relative h-44 w-full cursor-crosshair rounded-lg">
<ColorPickerAreaGradient className="rounded-lg" />
<ColorPickerAreaThumb className="h-4 w-4 rounded-full border-2 border-white shadow" />
</ColorPickerArea>Components with multiple inner elements expose a classNames prop:
<ColorPickerInput
className="flex items-center gap-1"
classNames={{
formatToggle: "rounded-md border px-2 h-8 text-xs",
field: "w-full rounded-md border px-2 h-8 text-sm",
}}
/>3. Unstyled (fully custom)
Use the components without importing any styles and target them with CSS selectors:
[data-cp-part="area"] { /* ... */ }
[data-cp-part="hue-slider"] [data-cp-el="thumb"] { /* ... */ }All components render data-cp-part and data-cp-el attributes for CSS targeting.
API Reference
Components
| Component | Dot Notation | Description |
|---|---|---|
ColorPicker |
- | Root provider. Wraps children in context and Radix Popover. |
ColorPickerTrigger |
ColorPicker.Trigger |
Button that opens the popover. Displays current color swatch. |
ColorPickerInputTrigger |
- | Input-style trigger with thumbnail, text input, format toggle, and eye dropper. |
ColorPickerContent |
ColorPicker.Content |
Popover content container with positioning. |
ColorPickerArea |
ColorPicker.Area |
2D saturation/brightness picker container. |
ColorPickerAreaGradient |
ColorPicker.AreaGradient |
Renders the white-to-black gradient overlays inside the area. |
ColorPickerAreaThumb |
ColorPicker.AreaThumb |
Draggable thumb positioned by saturation and brightness. |
ColorPickerHueSlider |
ColorPicker.HueSlider |
Hue slider container (0-360). |
ColorPickerHueSliderTrack |
ColorPicker.HueSliderTrack |
Rainbow gradient track. |
ColorPickerHueSliderThumb |
ColorPicker.HueSliderThumb |
Draggable hue thumb. |
ColorPickerAlphaSlider |
ColorPicker.AlphaSlider |
Alpha slider container (0-1). |
ColorPickerAlphaSliderTrack |
ColorPicker.AlphaSliderTrack |
Checkerboard + alpha gradient track. |
ColorPickerAlphaSliderThumb |
ColorPicker.AlphaSliderThumb |
Draggable alpha thumb. |
ColorPickerInput |
ColorPicker.Input |
Text input showing color in current format. Validates on blur/Enter. |
ColorPickerFormatToggle |
ColorPicker.FormatToggle |
Cycles between HEX, RGB, and HSL display formats. |
ColorPickerEyeDropper |
ColorPicker.EyeDropper |
Browser EyeDropper API button. Renders nothing in unsupported browsers. |
ColorPickerSwatches |
ColorPicker.Swatches |
Grid container for preset color swatches. |
ColorPickerSwatch |
ColorPicker.Swatch |
Individual color swatch button. |
ColorPickerModeSelector |
ColorPicker.ModeSelector |
Segmented control for switching between solid and gradient modes. |
ColorPickerModeSelectorItem |
ColorPicker.ModeSelectorItem |
Individual mode button (solid, linear, radial, conic, mesh). |
ColorPickerGradientEditor |
ColorPicker.GradientEditor |
Self-contained gradient editing UI with preview and stop manipulation. |
ColorPickerGradientSwatches |
ColorPicker.GradientSwatches |
Grid container for preset gradient swatches. |
ColorPickerGradientSwatch |
ColorPicker.GradientSwatch |
Individual gradient swatch button. |
ColorPickerContrastInfo |
ColorPicker.ContrastInfo |
WCAG contrast-ratio readout with level badge. Optionally opens a mini picker to change the reference color. |
ColorPickerContrastLine |
ColorPicker.ContrastLine |
SVG overlay for ColorPickerArea that draws the WCAG threshold curve and a dot pattern in the failing region. |
GradientPreview |
- | Lower-level gradient preview with stop dots and drag handles. |
GradientStops |
- | Lower-level horizontal stop bar with draggable markers. |
TokenList |
- | Lower-level searchable token list dropdown. |
ColorPickerProvider |
ColorPicker.Provider |
Context-only provider for solid colors (no Popover.Root). Used internally for per-stop color editing in gradient mode. |
Presets
Pre-composed components that bundle all controls together:
| Component | Description |
|---|---|
ColorPickerPopover |
Trigger + popover with all controls. |
ColorPickerInline |
Always-visible picker with all controls. |
ColorPickerControls |
Shared inner controls used by both presets. Can be used standalone inside a ColorPicker or ColorPickerProvider. |
Import from @markoradak/color-picker/presets or from the main entry.
ColorPicker Props
| Prop | Type | Default | Description |
|---|---|---|---|
value |
ColorPickerValue |
- | Controlled value (string or GradientValue). |
onValueChange |
(value: ColorPickerValue) => void |
- | Called when value changes. |
defaultValue |
ColorPickerValue |
"#000000" |
Initial value for uncontrolled mode. |
disabled |
boolean |
false |
Disables all interactions. |
defaultOpen |
boolean |
false |
Whether the popover starts open (uncontrolled). |
tokens |
ColorTokens |
- | Map of semantic token names to color values. |
autoTokens |
AutoTokensConfig |
true |
Auto-detect CSS custom property color tokens. true = detect all, false = disable, { prefix: "--brand-" } = filter by prefix. |
children |
ReactNode |
- | Compound sub-components. |
ColorPickerPopover Props
Includes all props from ColorPickerPresetProps plus popover-specific options:
| Prop | Type | Default | Description |
|---|---|---|---|
value |
ColorPickerValue |
- | Controlled value. |
onValueChange |
(value: ColorPickerValue) => void |
- | Value change callback. |
defaultValue |
ColorPickerValue |
"#000000" |
Initial uncontrolled value. |
disabled |
boolean |
false |
Disables all interactions. |
enableAlpha |
boolean |
true |
Show the alpha/opacity slider. |
enableGradient |
boolean |
true |
Show gradient editor controls. |
enableModeSelector |
boolean |
true when enableGradient is true |
Show the solid/gradient mode selector. |
enableEyeDropper |
boolean |
true |
Show EyeDropper button. |
enableFormatToggle |
boolean |
true |
Show format cycle button. |
enableTokenSearch |
boolean |
true |
Show search input in token dropdown. |
enableSwatches |
boolean |
true |
Show preset swatches. |
swatches |
string[] |
Built-in palette | Preset swatch colors (solid mode). |
swatchColumns |
number |
8 |
Grid columns for swatches. |
gradientSwatches |
GradientValue[] |
- | Preset gradient swatches (gradient mode). |
contrastColor |
string |
- | Reference color for the WCAG contrast row and threshold line (solid mode). When provided, contrast UI is rendered; omit to hide. |
onContrastColorChange |
(color: string) => void |
- | Called when the user changes the reference color via the contrast-indicator popover. |
tokens |
ColorTokens |
- | Map of semantic token names to color values. |
autoTokens |
AutoTokensConfig |
true |
Auto-detect CSS custom property tokens. |
side |
"top" | "right" | "bottom" | "left" |
"bottom" |
Popover placement. |
align |
"start" | "center" | "end" |
"center" |
Popover alignment. |
sideOffset |
number |
4 |
Offset from trigger (px). |
trigger |
ReactNode |
- | Custom trigger element (wraps in ColorPickerTrigger asChild). |
triggerMode |
"thumbnail" | "input" |
"thumbnail" |
Trigger style. "input" renders ColorPickerInputTrigger. Ignored when trigger is provided. |
className |
string |
- | Additional CSS class on the wrapper. |
ColorPickerInline Props
Same as ColorPickerPopover except without side, align, sideOffset, trigger, and triggerMode.
Utilities
| Function | Description |
|---|---|
toCSS(value) |
Convert a ColorPickerValue to a CSS string. |
fromCSS(css) |
Parse a CSS gradient string into a ColorPickerValue. Supports linear-gradient, radial-gradient, conic-gradient, and repeating-* variants. Returns the raw string for unparseable input. |
isGradient(value) |
Type guard: is the value a gradient object? |
isSolidColor(value) |
Type guard: is the value a solid color string? |
formatColor(input, format) |
Convert a color string to hex/rgb/hsl. |
detectFormat(input) |
Detect the format of a color string. |
parseColor(input) |
Parse any color string into a colord instance. |
isValidColor(input) |
Check if a string is a valid color. |
toHSVA(input) |
Convert a color string to HSVA. Returns white for invalid input. |
fromHSVA(hsva) |
Convert HSVA values back to a hex string. |
getContrastColor(bg) |
Returns "black" or "white" for best contrast against the given background. |
contrastRatio(a, b) |
Compute the WCAG 2.1 contrast ratio between two colors (1–21). |
getWcagLevel(ratio) |
Classify a ratio as "AAA", "AA", "AA18" (large text), or "Fail". Exported as type WcagLevel. |
getEffectiveBackgroundColor(element) |
Walk up the DOM from element and return the first non-transparent background color (falls back to white). |
colorLuminance(color) |
Relative luminance of any CSS color string (0–1). |
hsvLuminance(h, s, v, a) |
Relative luminance for HSVA values — useful for computing contrast boundaries across the picker area. |
contrastFromLuminances(l1, l2) |
Contrast ratio from two pre-computed luminances. |
resolveToken(value, tokens) |
Resolve a token name to its color value via the tokens map. Returns the value unchanged if not found. |
findMatchingToken(hex, tokens) |
Find the token name whose resolved color matches the given hex. |
getCSSColorTokens(prefix?) |
Scan the DOM for CSS custom properties that resolve to valid colors. |
createDefaultGradient(type) |
Create a default gradient of the given type. |
createDefaultGradientFromColor(type, color) |
Create a gradient from an existing color. |
createGradientStop(color, position) |
Create a gradient stop object with a generated ID. |
createMeshGradientStop(color, position, x, y) |
Create a mesh gradient stop with 2D coordinates. |
interpolateColorAt(stops, position) |
Interpolate a color at a position along the gradient. |
addStop(gradient, color, position) |
Add a stop to a gradient (returns new gradient). |
addStopWithCoordinates(gradient, color, position, x, y) |
Mesh-only: add a stop with explicit 2D coordinates. |
removeStop(gradient, stopId) |
Remove a stop by ID (returns new gradient). |
updateStop(gradient, stopId, updates) |
Update a stop's color or position (returns new gradient). |
moveStop(gradient, stopId, direction) |
Reorder a stop (mesh only): "front", "forward", "backward", "back". |
sortStops(stops) |
Sort stops by position (returns new array). |
clamp(n, min, max) |
Constrain a number to a range. |
getRelativePosition(event, element) |
Pointer position inside an element normalized to 0–1 on each axis. |
angleFromPosition(cx, cy, x, y) |
Angle in degrees from a center point to an (x, y) coordinate. |
Hooks
| Hook | Description |
|---|---|
useColorPicker(options) |
Core state management hook. Manages HSVA state, format cycling, controlled/uncontrolled value, and token resolution. |
useGradient(options) |
Gradient state management. Manages active stop, stop CRUD, and gradient replacement. |
usePointerDrag(options) |
Generic pointer drag hook used by Area and Slider components. |
useAutoTokens(config, manualTokens) |
Merges auto-detected CSS custom property tokens with manually provided tokens. |
useTokenDropdown(options) |
State machine for the token dropdown: open/close, search, keyboard navigation, click-outside dismissal. |
Types
type SolidColor = string;
type ColorFormat = "hex" | "rgb" | "hsl" | "oklch";
type GradientType = "linear" | "radial" | "conic" | "mesh";
type ColorPickerMode = "solid" | GradientType;
type ColorPickerValue = SolidColor | GradientValue;
type ColorTokens = Record<string, string>;
type AutoTokensConfig = { prefix?: string } | boolean;
// GradientValue is a discriminated union -- switch on `value.type`
type GradientValue =
| LinearGradientValue
| RadialGradientValue
| ConicGradientValue
| MeshGradientValue;
interface LinearGradientValue {
type: "linear";
stops: GradientStop[];
angle: number; // degrees
}
interface RadialGradientValue {
type: "radial";
stops: GradientStop[];
centerX: number; // 0-100
centerY: number; // 0-100
}
interface ConicGradientValue {
type: "conic";
stops: GradientStop[];
angle: number; // degrees
centerX: number; // 0-100
centerY: number; // 0-100
}
interface MeshGradientValue {
type: "mesh";
stops: MeshGradientStop[];
baseColor?: string; // solid color behind radial blobs
}
interface GradientStop {
id: string;
color: string;
position: number; // 0-100
}
interface MeshGradientStop {
id: string;
color: string;
position: number; // 0-100
x: number; // 0-100
y: number; // 0-100
}
interface HSVA {
h: number; // 0-360
s: number; // 0-100
v: number; // 0-100
a: number; // 0-1
}CSS Theming
Import the default stylesheet and override CSS custom properties:
@import "@markoradak/color-picker/styles";
.my-theme {
--cp-bg: #1a1a2e;
--cp-border: #2a2a4a;
--cp-border-focus: #60a5fa;
--cp-text: #f5f5f5;
--cp-radius: 12px;
--cp-width: 280px;
}Dark Mode
Dark mode is applied automatically via prefers-color-scheme: dark. You can also use a .dark class on any ancestor element for manual toggling:
<div class="dark">
<!-- All color pickers inside will use dark theme -->
</div>Available Custom Properties
| Property | Default (Light) | Description |
|---|---|---|
--cp-bg |
#ffffff |
Panel background |
--cp-bg-secondary |
#fafafa |
Secondary background (mode selector, token badge) |
--cp-border |
#e5e5e5 |
Border color |
--cp-border-focus |
#3b82f6 |
Focus ring / active indicator color |
--cp-radius |
0.75rem |
Border radius (panel) |
--cp-radius-sm |
0.5rem |
Border radius (controls) |
--cp-radius-full |
9999px |
Border radius (thumbs, swatches) |
--cp-text |
#171717 |
Primary text color |
--cp-text-muted |
#737373 |
Muted text color |
--cp-input-bg |
#ffffff |
Input background |
--cp-input-border |
#d4d4d4 |
Input border |
--cp-shadow |
- | Panel box-shadow |
--cp-shadow-sm |
- | Small box-shadow (mode selector active) |
--cp-ring |
0 0 0 2px var(--cp-border-focus) |
Focus ring box-shadow |
--cp-ring-offset |
2px |
Focus ring outline offset |
--cp-hover-bg |
#f5f5f5 |
Hover background |
--cp-active-border |
#1f2937 |
Active swatch border |
--cp-width |
18rem |
Panel width |
--cp-area-height |
11rem |
Color area height |
--cp-slider-height |
0.75rem |
Slider track height |
--cp-thumb-size |
1rem |
Thumb size |
--cp-swatch-size |
1.75rem |
Swatch button size |
--cp-gap |
0.75rem |
Spacing between controls |
--cp-padding |
0.75rem |
Panel padding |
--cp-checkerboard-color |
#e5e5e5 |
Checkerboard pattern color |
--cp-z-index-dropdown |
50 |
z-index for dropdowns |
--cp-z-index-portal |
99999 |
z-index for portals |
--cp-font-family |
monospace |
Font for color value inputs |
--cp-transition-duration |
0.15s |
Transition duration (respects prefers-reduced-motion) |
Accessibility
- Full keyboard navigation (arrow keys, Enter, Escape, Tab)
- ARIA roles and labels on all interactive elements
prefers-reduced-motion: reducedisables all animations- Focus-visible rings on all focusable elements
- Screen reader announcements for color changes
Browser Support
- All modern browsers (Chrome, Firefox, Safari, Edge)
- The EyeDropper button uses the EyeDropper API (Chromium-based browsers). It automatically hides in unsupported browsers.
Development
This package lives in a pnpm + Turborepo monorepo alongside a Next.js demo site. Requires Node 18+ and pnpm 10+.
packages/
react/ @markoradak/color-picker — this package
apps/
web/ Next.js demo + playground sitepnpm install
pnpm dev # tsup --watch + next dev, concurrently
pnpm build # build the package, then the demo site
pnpm test # 261 tests in packages/react
pnpm typecheck # typecheck all workspacesLicense
MIT