rishimohan/colorshot

Color and Gradient Picker Component for React and Vue

TypeScript

3

23 commits

updated Oct 2, 2026

See the code

See what people are saying

SourceMessageScoreDate

I open-sourced the color and gradient picker from our design editor (React + Vue) (r/reactjs)

I needed a picker for Orshot's template editor that wouldn't break on real CSS. The one we used threw on valid values and had no room for brand or saved colors, so I built my own \- Takes any CSS color or gradient string and returns one, untouched parts stay exactly as written \- Linear, radial,…

1

Oct 4, 2026

Show HN: I open-sourced the gradient picker from our design editor

1

Oct 4, 2026

README

Colorshot, a color and gradient picker for React and Vue

Colorshot
Color and gradient picker for React and Vue.
Solid colors, linear, radial and conic gradients, swatch groups, one package.

Introduction

Colorshot is the color picker inside Orshot Studio, open sourced. It takes any CSS color or gradient string and gives one back, so it fits whatever your app already stores. It never throws on input it cannot read, and the parts of a value you did not edit come back exactly as written.

  • Solid colors with hue, saturation, alpha, eyedropper and hex / rgb / hsl / oklch input
  • Linear, radial, conic and repeating gradients with draggable stops, an angle dial and a center pad
  • Paste any CSS color or gradient with Cmd/Ctrl+V, copy with Cmd/Ctrl+C
  • Your own swatch groups: brand, saved, recent, with search, add, remove, reorder and rename
  • A ready-made ColorPicker, a ColorField that opens it in a popover, or headless parts to build your own layout
  • Keyboard, screen reader, touch and two-finger trackpad support
  • React and Vue from one framework-free core, themed with CSS variables

Install

npm i @orshot/colorshot

Getting started

"use client";

import { useState } from "react";
import { ColorPicker } from "@orshot/colorshot/react";
import "@orshot/colorshot/styles.css";

export function FillPicker() {
  const [fill, setFill] = useState("linear-gradient(135deg, #3E5CEB 0%, #22C55E 100%)");

  return (
    <ColorPicker
      value={fill}
      onChange={setFill} // every frame while dragging
      onChangeComplete={(value) => save(value)} // once, when the change is done
    />
  );
}

Vue:

<script setup>
import { ref } from "vue";
import { ColorPicker } from "@orshot/colorshot/vue";
import "@orshot/colorshot/styles.css";

const fill = ref("#3E5CEB");
</script>

<template>
  <ColorPicker v-model="fill" @change-complete="save" />
</template>

Examples

Live versions of every example: https://orshot.com/open-source/colorshot#linear

Linear, radial or conic gradient only

One mode hides the tabs. value is any CSS gradient, and the picker writes one back.

<ColorPicker value={value} onChange={setValue} modes={["linear"]} />
// value: "linear-gradient(90deg, #22C55E 0%, #3E5CEB 100%)"

<ColorPicker value={value} onChange={setValue} modes={["radial"]} />
// value: "radial-gradient(circle at 30% 30%, #FDE68A 0%, #F97316 45%, #BE123C 100%)"

<ColorPicker value={value} onChange={setValue} modes={["conic"]} />
// value: "conic-gradient(from 0deg at 50% 50%, #EF4444, #F59E0B, #22C55E, #3B82F6, #A855F7, #EF4444)"

Repeating gradients (repeating-linear-gradient(...) and the rest) work the same way; toggle Repeating in the ... menu.

Gradients only

For backgrounds that must be a gradient. defaultGradient is what a solid value turns into.

<ColorPicker
  value={value}
  onChange={setValue}
  modes={["linear", "radial", "conic"]}
  defaultGradient="linear-gradient(90deg, #3E5CEB 0%, #22C55E 100%)"
  gradientPresets
/>

Solid colors, no transparency

<ColorPicker value={value} onChange={setValue} modes={["solid"]} alpha={false} formats={["hex", "rgb", "hsl"]} />

Text color with a contrast check

Shows the WCAG ratio and draws the AA line on the color area.

<ColorPicker value={value} onChange={setValue} modes={["solid"]} alpha={false} contrastWith="#FFFFFF" />

OKLCH and wide gamut

<ColorPicker
  value={value} // "oklch(0.62 0.2 265)"
  onChange={setValue}
  modes={["solid"]}
  space="oklch"
  formats={["oklch", "hex", "rgb"]}
  outputFormat="oklch"
/>

Undo and before / after

<ColorPicker value={value} onChange={setValue} history compare />

Saving changes

onChange fires on every frame of a drag. Update the preview there, and save in onChangeComplete, which fires once.

<ColorPicker
  value={layer.fill}
  onChange={(fill) => updateLayerPreview(layer.id, { fill })}
  onChangeComplete={(fill) => saveLayer(layer.id, { fill })}
/>

In a form

ColorField takes a value and an onChange like an input, so it works with any form library:

import { Controller, useForm } from "react-hook-form";
import { ColorField } from "@orshot/colorshot/react";

const { control, handleSubmit } = useForm({ defaultValues: { accent: "#3E5CEB" } });

<Controller
  name="accent"
  control={control}
  render={({ field }) => (
    <ColorField label="Accent" value={field.value} onChange={field.onChange} modes={["solid"]} alpha={false} />
  )}
/>;

Match a stored format

Keep whatever format your app already stores. Here: uppercase hex when opaque, comma rgba() otherwise.

import { formatColor } from "@orshot/colorshot";

const outputFormat = (color) =>
  color.alpha >= 1
    ? formatColor({ ...color, alpha: 1 }, "hex", { upper: true })
    : formatColor(color, "rgb", { legacy: true, fn: "rgba" });

<ColorPicker value={value} onChange={setValue} outputFormat={outputFormat} />;

shadcn/ui theme

/* globals.css. On shadcn/ui v3 (HSL channel tokens) wrap them: hsl(var(--primary)) */
[data-colorshot],
[data-colorshot-field] {
  --cs-accent: var(--primary);
  --cs-bg: var(--popover);
  --cs-fg: var(--popover-foreground);
  --cs-border: var(--border);
  --cs-radius: var(--radius);
}

Using with AI

Install the Colorshot skill to teach your coding agent (Claude Code, Codex, Cursor, GitHub Copilot and others) how to add and configure the picker:

npx skills add rishimohan/colorshot

The API reference is also available for LLMs in llms.txt, and the docs page has a setup prompt you can copy into any agent.

One package

ImportWhat it is
@orshot/colorshot/reactReact components and composable parts
@orshot/colorshot/vueVue components and composable parts
@orshot/colorshotFramework-free helpers: color math, CSS gradient parsing, contrast, gradient handles, the picker store. Safe in server code
@orshot/colorshot/styles.cssThe stylesheet, imported once

Apps only bundle what they import: a React app never pulls in the Vue code, and React and Vue are optional peer dependencies.

Optional features

All off unless you turn them on.

PropWhat it does
space="oklch"OKLCH area (lightness × chroma at one hue, drawn in Display P3 where supported, dashed line marks the sRGB edge) and a perceptual hue slider
historyUndo / redo committed changes with Cmd/Ctrl+Z inside the picker
swatchSearchSearch box that filters every swatch group by name, value or hex
gradientPresetsGradient swatches shown in gradient modes (true or your own list)
contrastWith="#fff"WCAG contrast badge against a background; gradients use their weakest stop
tabIconsIcons next to the Solid / Linear / Radial / Conic labels
compareBefore / after swatch beside the sliders; click "before" to restore
swatchLayout="stack"List every swatch group instead of one tab per group
size="sm"Compact: 240px wide, 24px controls
variant="inset"Mode tabs first and a framed area, instead of the default edge-to-edge area
shortcuts={false}Turn off 1-4 (modes) and I (eyedropper)
storageKey={null}Keep recent colors, the chosen format and swatch tab in memory only
swatches=[...]Swatch groups with onAdd / onRemove, limit, recent: true, showIn
eyeDropper={fn}Your own screen picker for browsers without the native EyeDropper

Always on, no props needed:

  • Cmd/Ctrl+C copies the value and Cmd/Ctrl+V applies a pasted color or gradient while the picker has focus
  • Two-finger trackpad swipes move the area, sliders, stops and angle dial (the page still scrolls when the swipe started elsewhere)
  • Alt-drag a stop to duplicate it, drag a stop off the bar or press Delete to remove it, drag a solid swatch onto the bar to add it
  • Switching between solid and gradient keeps the color you are editing on the active stop
  • Touch screens get larger thumbs and targets

ColorField

A trigger that opens the picker in a popover. Flips and shifts to stay on screen, closes on Escape, outside click or tab-away, and returns focus to the trigger. Takes every ColorPicker prop.

import { ColorField } from "@orshot/colorshot/react";

<ColorField label="Fill" value={fill} onChange={setFill} gradientPresets history />;

On-canvas gradient handles

Framework-free helpers for drawing gradient handles on your own canvas:

import { gradientHandles, moveGradientHandle } from "@orshot/colorshot";

const result = gradientHandles(value, width, height); // null when value is not a gradient
if (result) {
  const { handles, line } = result; // start / end / stops / center / angle
  const next = moveGradientHandle(value, handles[0].id, x, y, width, height, { snap: shiftKey });
}

API reference

ColorPicker props

ColorPicker (React) and <ColorPicker> (Vue, with v-model and @change-complete) take the same props.

PropTypeDefaultWhat it does
valuestringAny CSS color or gradient. Unknown text is shown as unsupported and left as it is
defaultValuestringUncontrolled starting value
onChange(value: string) => voidFires while dragging or typing, at most once per frame
onChangeComplete(value: string) => voidFires once when a change ends (pointer up, input commit, swatch click). Save and record undo here
modesPickerMode[]all fourAny of "solid", "linear", "radial", "conic". One mode hides the tabs
outputFormatOutputFormat"preserve""preserve" keeps each color's format; or a format name ("hex", "rgb", "oklch", ...); or (color, source) => string
defaultColorstringThe color a gradient turns into when switched to solid
defaultGradientstringThe gradient a solid color turns into when switched to a gradient mode
alphabooleantrueOpacity slider and field
formatsDisplayFormat[]hex, rgb, hsl, oklchFormats in the input menu: hex, rgb, hsl, hsb, oklch, oklab, lch, lab, p3, cmyk
defaultFormatDisplayFormatfrom the valueStarting format in the input menu
inputsbooleantrueShow the format menu and channel fields
eyeDropperboolean | EyeDropperFntrueNative EyeDropper where it exists; pass a function for other browsers; false hides it
swatchesSwatchGroupConfig[]Swatch groups (see below)
swatchSearchbooleanfalseSearch box over every swatch group
swatchLayout"tabs" | "stack""tabs" with 2+ groupsOne tab per group, or every group listed
gradientPresetsboolean | string[]falseGradient swatches in gradient modes: the built-in set or your own
interpolationbooleantrueColor blending menu (in oklch and others) for gradients
contrastWithstringWCAG contrast badge and AA line against this background
comparebooleanfalseBefore / after swatch; click "before" to restore
historyboolean | { limit?: number }falseUndo / redo with Cmd/Ctrl+Z inside the picker
space"hsv" | "oklch""hsv"Color model of the area and hue slider
tabIconsbooleanfalseIcons next to the mode labels
theme"light" | "dark"follows the OSForce a theme
size"sm"Compact: 240px wide, 24px controls
variant"bleed" | "inset""bleed"Edge-to-edge area, or mode tabs first and a framed area
shortcutsbooleantrue1-4 switch modes, I opens the eyedropper (while the picker has focus)
storageKeystring | null"colorshot"localStorage prefix for recent colors and the chosen format; null keeps them in memory
labelsPartial<Labels>EnglishOverride any visible or screen reader text
className, styleOn the root element; set --cs-* variables through style

Swatch groups

interface Swatch {
  value: string;
  label?: string; // shown as a tooltip
  id?: string; // your id, passed back to onRemove
}

interface SwatchGroupConfig {
  id: string;
  label?: ReactNode; // the group's tab name
  colors?: (string | Swatch)[] | "default"; // "default" is the built-in palette
  recent?: boolean; // recently used colors, stored under storageKey
  limit?: number; // show at most this many, with a "Show all" toggle
  showIn?: "solid" | "gradient"; // only show the group in one kind of mode
  onAdd?: (value: string) => void; // shows a + button that saves the current value
  onRemove?: (swatch: Swatch) => void; // remove button on hover, Delete on a focused swatch
  onReorder?: (swatches: Swatch[]) => void; // drag (or Alt + arrows) to reorder
  onRename?: (swatch: Swatch, label: string) => void; // double-click or F2
  renderSwatch?: (swatch: Swatch, state: { selected: boolean }) => ReactNode;
  emptyText?: ReactNode; // shown when the group is empty
}

ColorField props

Every ColorPicker prop, plus:

PropTypeWhat it does
labelstringAccessible name of the trigger, e.g. "Fill"
placeholderstringText when the value is empty
placement"bottom-start" | "bottom-end" | "top-start" | "top-end" | "right-start" | "left-start"Preferred side (default bottom-start). It flips and shifts to stay on screen
offsetnumberGap between the trigger and the picker in px (default 6)
open, defaultOpen, onOpenChangeboolean, (open) => voidControl the popover, or leave it uncontrolled
portalboolean | HTMLElement | nullRender in a portal on document.body (default) or inside an element; false renders inline
disabledbooleanDisable the trigger
renderTrigger({ value, open }) => ReactNodeYour own trigger contents
pickerClassName, pickerStylestring, CSSPropertiesStyle the picker inside the popover

A ref gives you { open(), close(), trigger }.

Parts

For your own layout, compose parts under Picker.Root (it takes the value props above):

PartWhat it is
RootThe panel and the state every other part reads
AreaThe 2D color area
Hue, AlphaHue and opacity sliders
ModeTabsSolid, Linear, Radial and Conic tabs
GradientEditorBar plus controls, shown only in gradient modes
GradientBarThe stop bar on its own
GradientControls, AngleDial, CenterPadAngle, shape, size and center controls
InputsFormat menu and channel fields
EyeDropperScreen color picking where the browser supports it
SwatchesSwatch groups with tabs, search, add and remove
CurrentSwatch, PreviewThe current value, with an optional before and after
ContrastWCAG contrast badge against a background
NoticeShown when a value cannot be read
import { Picker } from "@orshot/colorshot/react";

<Picker.Root value={value} onChange={setValue}>
  <Picker.Area />
  <Picker.Hue />
  <Picker.Alpha />
  <Picker.Inputs />
  <Picker.Swatches groups={groups} />
</Picker.Root>;

Theming

Set CSS variables on any parent or on the picker. Styles live in @layer colorshot, so your own unlayered CSS (Tailwind utilities included) wins without !important.

VariableWhat it sets
--cs-accentFocus rings and selected states
--cs-bg, --cs-fgPanel background and text
--cs-border, --cs-dividerBorders and dividers
--cs-radius, --cs-radius-controlPanel and control corners
--cs-width, --cs-padding, --cs-gapPanel size and spacing
--cs-area-heightHeight of the color area
--cs-swatch-columnsSwatches per row
--cs-shadowPanel shadow
--cs-font-sizeBase text size
--cs-motionTransition length (motion is off under prefers-reduced-motion)
.inspector [data-colorshot] {
  --cs-accent: #3e5ceb;
  --cs-width: 100%;
  --cs-area-height: 160px;
}

The documented --cs-* variables, data-part names and the data-state, data-mode, data-theme, data-size and data-variant attributes follow semver.

Localization

Every visible and screen reader string is a label. {placeholders} are filled in for you.

<ColorPicker
  value={value}
  onChange={setValue}
  labels={{
    picker: "Farbwähler",
    solid: "Einfarbig",
    linear: "Linear",
    radial: "Radial",
    conic: "Konisch",
    hue: "Farbton",
    alpha: "Deckkraft",
    stopName: "Farbstopp {index} von {count}",
  }}
/>

Keyboard and gestures

InputWhat it does
Two-finger swipeMove the area, sliders, stops and angle on a trackpad
Arrow keysNudge the focused thumb, stop or field (Shift for 10x)
1 to 4Switch Solid, Linear, Radial and Conic (picker focused)
IOpen the eyedropper
Cmd/Ctrl+C, Cmd/Ctrl+VCopy the value, or paste any CSS color or gradient
Cmd/Ctrl+Z, Shift+Cmd/Ctrl+ZUndo and redo, with the history prop
Click the bar, EnterAdd a stop
Alt + drag a stopCopy the stop
Drag off the bar, DeleteRemove a stop
Drag a swatch onto the barAdd it as a stop
EscapeDrop what you typed, or close a ColorField

Core helpers

@orshot/colorshot has no framework code and is safe in server code.

HelperWhat it does
createPicker(options)The framework-free store the React and Vue pickers use
parseColor(css)Any CSS color to { color, format }, or null. Never throws
formatColor(color, format, style?)Write a color as hex, rgb, hsl, oklch, p3 and more
convert, toSrgb, inGamut, toGamutColor space conversion and gamut mapping
isGradient, parseGradient, serializeGradientRead and write CSS gradients, keeping what you did not edit
gradientAngle, gradientCenterRead a gradient's angle and center
gradientHandles, moveGradientHandleStart, end, stop and center handles for your own canvas
contrastRatio(fg, bg), contrastLevel(ratio), luminanceWCAG contrast and its AA or AAA level
isSafeCssValue, safeCssValueOnly plain colors and gradients, before a stored value reaches a style
import { parseColor, formatColor, contrastRatio, isSafeCssValue } from "@orshot/colorshot";

formatColor(parseColor("#3366ff")!.color, "oklch"); // "oklch(0.5726 0.2338 265.28)"
contrastRatio("#64748b", "#ffffff"); // 4.76
isSafeCssValue(storedValue); // validate stored values before writing them into HTML or CSS

TypeScript

Types ship with the package, for every entry.

import type { ColorPickerProps, ColorFieldProps, SwatchGroupConfig, Swatch, DisplayFormat } from "@orshot/colorshot/react";
import type { OutputFormat, PickerMode, Color } from "@orshot/colorshot";

Next.js and SSR

The React entry is marked "use client": render the components from client components, and import helpers from @orshot/colorshot in server code. The picker renders the same markup on the server and the client. Pass theme from your theme provider after hydration, or leave it unset to follow the OS.

Accessibility

  • Checked with axe against WCAG 2.2 AA in the browser test suite
  • Every slider, stop and field is keyboard reachable, and sliders announce their value in words through aria-valuetext
  • Screen reader labels for every control, all overridable through labels
  • High contrast (forced-colors), right-to-left layouts and prefers-reduced-motion are supported
  • Touch screens get larger thumbs and targets

Browser support

Current Chrome, Edge, Firefox and Safari. The browser tests run in Chromium, Firefox and WebKit. The eyedropper uses the native EyeDropper API where it exists (Chromium) and hides itself elsewhere, unless you pass eyeDropper={fn}.

Development

pnpm install
pnpm --filter @colorshot/playground dev       # playground on http://localhost:5190
pnpm --filter @colorshot/core test            # parser, conversion and gradient corpus tests
pnpm --filter @colorshot/playground test:e2e  # browser tests on Chromium, Firefox and WebKit
pnpm build && pnpm size                       # build everything and check gzip budgets

See CONTRIBUTING.md for the layout and the rules, and RELEASING.md for publishing.

License

MIT. Free for personal and commercial use: keep the copyright line and license text, which link to https://orshot.com/open-source/colorshot. The color conversion matrices come from CSS Color Module Level 4, see THIRD-PARTY-NOTICES.md.


Brought to you by Orshot, the API for automated image, PDF and video generation from templates. MIT License.

color-picker
gradient
gradient-picker
oklch
react
react-color-picker
shadcn
typescript
vue
vue-color-picker

rishimohan/colorshot

Color and Gradient Picker Component for React and Vue

TypeScript

3

23 commits

updated Oct 2, 2026

See the code

See what people are saying

SourceMessageScoreDate

I open-sourced the color and gradient picker from our design editor (React + Vue) (r/reactjs)

I needed a picker for Orshot's template editor that wouldn't break on real CSS. The one we used threw on valid values and had no room for brand or saved colors, so I built my own \- Takes any CSS color or gradient string and returns one, untouched parts stay exactly as written \- Linear, radial,…

1

Oct 4, 2026

Show HN: I open-sourced the gradient picker from our design editor

1

Oct 4, 2026

README

Colorshot, a color and gradient picker for React and Vue

Colorshot
Color and gradient picker for React and Vue.
Solid colors, linear, radial and conic gradients, swatch groups, one package.

Introduction

Colorshot is the color picker inside Orshot Studio, open sourced. It takes any CSS color or gradient string and gives one back, so it fits whatever your app already stores. It never throws on input it cannot read, and the parts of a value you did not edit come back exactly as written.

  • Solid colors with hue, saturation, alpha, eyedropper and hex / rgb / hsl / oklch input
  • Linear, radial, conic and repeating gradients with draggable stops, an angle dial and a center pad
  • Paste any CSS color or gradient with Cmd/Ctrl+V, copy with Cmd/Ctrl+C
  • Your own swatch groups: brand, saved, recent, with search, add, remove, reorder and rename
  • A ready-made ColorPicker, a ColorField that opens it in a popover, or headless parts to build your own layout
  • Keyboard, screen reader, touch and two-finger trackpad support
  • React and Vue from one framework-free core, themed with CSS variables

Install

npm i @orshot/colorshot

Getting started

"use client";

import { useState } from "react";
import { ColorPicker } from "@orshot/colorshot/react";
import "@orshot/colorshot/styles.css";

export function FillPicker() {
  const [fill, setFill] = useState("linear-gradient(135deg, #3E5CEB 0%, #22C55E 100%)");

  return (
    <ColorPicker
      value={fill}
      onChange={setFill} // every frame while dragging
      onChangeComplete={(value) => save(value)} // once, when the change is done
    />
  );
}

Vue:

<script setup>
import { ref } from "vue";
import { ColorPicker } from "@orshot/colorshot/vue";
import "@orshot/colorshot/styles.css";

const fill = ref("#3E5CEB");
</script>

<template>
  <ColorPicker v-model="fill" @change-complete="save" />
</template>

Examples

Live versions of every example: https://orshot.com/open-source/colorshot#linear

Linear, radial or conic gradient only

One mode hides the tabs. value is any CSS gradient, and the picker writes one back.

<ColorPicker value={value} onChange={setValue} modes={["linear"]} />
// value: "linear-gradient(90deg, #22C55E 0%, #3E5CEB 100%)"

<ColorPicker value={value} onChange={setValue} modes={["radial"]} />
// value: "radial-gradient(circle at 30% 30%, #FDE68A 0%, #F97316 45%, #BE123C 100%)"

<ColorPicker value={value} onChange={setValue} modes={["conic"]} />
// value: "conic-gradient(from 0deg at 50% 50%, #EF4444, #F59E0B, #22C55E, #3B82F6, #A855F7, #EF4444)"

Repeating gradients (repeating-linear-gradient(...) and the rest) work the same way; toggle Repeating in the ... menu.

Gradients only

For backgrounds that must be a gradient. defaultGradient is what a solid value turns into.

<ColorPicker
  value={value}
  onChange={setValue}
  modes={["linear", "radial", "conic"]}
  defaultGradient="linear-gradient(90deg, #3E5CEB 0%, #22C55E 100%)"
  gradientPresets
/>

Solid colors, no transparency

<ColorPicker value={value} onChange={setValue} modes={["solid"]} alpha={false} formats={["hex", "rgb", "hsl"]} />

Text color with a contrast check

Shows the WCAG ratio and draws the AA line on the color area.

<ColorPicker value={value} onChange={setValue} modes={["solid"]} alpha={false} contrastWith="#FFFFFF" />

OKLCH and wide gamut

<ColorPicker
  value={value} // "oklch(0.62 0.2 265)"
  onChange={setValue}
  modes={["solid"]}
  space="oklch"
  formats={["oklch", "hex", "rgb"]}
  outputFormat="oklch"
/>

Undo and before / after

<ColorPicker value={value} onChange={setValue} history compare />

Saving changes

onChange fires on every frame of a drag. Update the preview there, and save in onChangeComplete, which fires once.

<ColorPicker
  value={layer.fill}
  onChange={(fill) => updateLayerPreview(layer.id, { fill })}
  onChangeComplete={(fill) => saveLayer(layer.id, { fill })}
/>

In a form

ColorField takes a value and an onChange like an input, so it works with any form library:

import { Controller, useForm } from "react-hook-form";
import { ColorField } from "@orshot/colorshot/react";

const { control, handleSubmit } = useForm({ defaultValues: { accent: "#3E5CEB" } });

<Controller
  name="accent"
  control={control}
  render={({ field }) => (
    <ColorField label="Accent" value={field.value} onChange={field.onChange} modes={["solid"]} alpha={false} />
  )}
/>;

Match a stored format

Keep whatever format your app already stores. Here: uppercase hex when opaque, comma rgba() otherwise.

import { formatColor } from "@orshot/colorshot";

const outputFormat = (color) =>
  color.alpha >= 1
    ? formatColor({ ...color, alpha: 1 }, "hex", { upper: true })
    : formatColor(color, "rgb", { legacy: true, fn: "rgba" });

<ColorPicker value={value} onChange={setValue} outputFormat={outputFormat} />;

shadcn/ui theme

/* globals.css. On shadcn/ui v3 (HSL channel tokens) wrap them: hsl(var(--primary)) */
[data-colorshot],
[data-colorshot-field] {
  --cs-accent: var(--primary);
  --cs-bg: var(--popover);
  --cs-fg: var(--popover-foreground);
  --cs-border: var(--border);
  --cs-radius: var(--radius);
}

Using with AI

Install the Colorshot skill to teach your coding agent (Claude Code, Codex, Cursor, GitHub Copilot and others) how to add and configure the picker:

npx skills add rishimohan/colorshot

The API reference is also available for LLMs in llms.txt, and the docs page has a setup prompt you can copy into any agent.

One package

ImportWhat it is
@orshot/colorshot/reactReact components and composable parts
@orshot/colorshot/vueVue components and composable parts
@orshot/colorshotFramework-free helpers: color math, CSS gradient parsing, contrast, gradient handles, the picker store. Safe in server code
@orshot/colorshot/styles.cssThe stylesheet, imported once

Apps only bundle what they import: a React app never pulls in the Vue code, and React and Vue are optional peer dependencies.

Optional features

All off unless you turn them on.

PropWhat it does
space="oklch"OKLCH area (lightness × chroma at one hue, drawn in Display P3 where supported, dashed line marks the sRGB edge) and a perceptual hue slider
historyUndo / redo committed changes with Cmd/Ctrl+Z inside the picker
swatchSearchSearch box that filters every swatch group by name, value or hex
gradientPresetsGradient swatches shown in gradient modes (true or your own list)
contrastWith="#fff"WCAG contrast badge against a background; gradients use their weakest stop
tabIconsIcons next to the Solid / Linear / Radial / Conic labels
compareBefore / after swatch beside the sliders; click "before" to restore
swatchLayout="stack"List every swatch group instead of one tab per group
size="sm"Compact: 240px wide, 24px controls
variant="inset"Mode tabs first and a framed area, instead of the default edge-to-edge area
shortcuts={false}Turn off 1-4 (modes) and I (eyedropper)
storageKey={null}Keep recent colors, the chosen format and swatch tab in memory only
swatches=[...]Swatch groups with onAdd / onRemove, limit, recent: true, showIn
eyeDropper={fn}Your own screen picker for browsers without the native EyeDropper

Always on, no props needed:

  • Cmd/Ctrl+C copies the value and Cmd/Ctrl+V applies a pasted color or gradient while the picker has focus
  • Two-finger trackpad swipes move the area, sliders, stops and angle dial (the page still scrolls when the swipe started elsewhere)
  • Alt-drag a stop to duplicate it, drag a stop off the bar or press Delete to remove it, drag a solid swatch onto the bar to add it
  • Switching between solid and gradient keeps the color you are editing on the active stop
  • Touch screens get larger thumbs and targets

ColorField

A trigger that opens the picker in a popover. Flips and shifts to stay on screen, closes on Escape, outside click or tab-away, and returns focus to the trigger. Takes every ColorPicker prop.

import { ColorField } from "@orshot/colorshot/react";

<ColorField label="Fill" value={fill} onChange={setFill} gradientPresets history />;

On-canvas gradient handles

Framework-free helpers for drawing gradient handles on your own canvas:

import { gradientHandles, moveGradientHandle } from "@orshot/colorshot";

const result = gradientHandles(value, width, height); // null when value is not a gradient
if (result) {
  const { handles, line } = result; // start / end / stops / center / angle
  const next = moveGradientHandle(value, handles[0].id, x, y, width, height, { snap: shiftKey });
}

API reference

ColorPicker props

ColorPicker (React) and <ColorPicker> (Vue, with v-model and @change-complete) take the same props.

PropTypeDefaultWhat it does
valuestringAny CSS color or gradient. Unknown text is shown as unsupported and left as it is
defaultValuestringUncontrolled starting value
onChange(value: string) => voidFires while dragging or typing, at most once per frame
onChangeComplete(value: string) => voidFires once when a change ends (pointer up, input commit, swatch click). Save and record undo here
modesPickerMode[]all fourAny of "solid", "linear", "radial", "conic". One mode hides the tabs
outputFormatOutputFormat"preserve""preserve" keeps each color's format; or a format name ("hex", "rgb", "oklch", ...); or (color, source) => string
defaultColorstringThe color a gradient turns into when switched to solid
defaultGradientstringThe gradient a solid color turns into when switched to a gradient mode
alphabooleantrueOpacity slider and field
formatsDisplayFormat[]hex, rgb, hsl, oklchFormats in the input menu: hex, rgb, hsl, hsb, oklch, oklab, lch, lab, p3, cmyk
defaultFormatDisplayFormatfrom the valueStarting format in the input menu
inputsbooleantrueShow the format menu and channel fields
eyeDropperboolean | EyeDropperFntrueNative EyeDropper where it exists; pass a function for other browsers; false hides it
swatchesSwatchGroupConfig[]Swatch groups (see below)
swatchSearchbooleanfalseSearch box over every swatch group
swatchLayout"tabs" | "stack""tabs" with 2+ groupsOne tab per group, or every group listed
gradientPresetsboolean | string[]falseGradient swatches in gradient modes: the built-in set or your own
interpolationbooleantrueColor blending menu (in oklch and others) for gradients
contrastWithstringWCAG contrast badge and AA line against this background
comparebooleanfalseBefore / after swatch; click "before" to restore
historyboolean | { limit?: number }falseUndo / redo with Cmd/Ctrl+Z inside the picker
space"hsv" | "oklch""hsv"Color model of the area and hue slider
tabIconsbooleanfalseIcons next to the mode labels
theme"light" | "dark"follows the OSForce a theme
size"sm"Compact: 240px wide, 24px controls
variant"bleed" | "inset""bleed"Edge-to-edge area, or mode tabs first and a framed area
shortcutsbooleantrue1-4 switch modes, I opens the eyedropper (while the picker has focus)
storageKeystring | null"colorshot"localStorage prefix for recent colors and the chosen format; null keeps them in memory
labelsPartial<Labels>EnglishOverride any visible or screen reader text
className, styleOn the root element; set --cs-* variables through style

Swatch groups

interface Swatch {
  value: string;
  label?: string; // shown as a tooltip
  id?: string; // your id, passed back to onRemove
}

interface SwatchGroupConfig {
  id: string;
  label?: ReactNode; // the group's tab name
  colors?: (string | Swatch)[] | "default"; // "default" is the built-in palette
  recent?: boolean; // recently used colors, stored under storageKey
  limit?: number; // show at most this many, with a "Show all" toggle
  showIn?: "solid" | "gradient"; // only show the group in one kind of mode
  onAdd?: (value: string) => void; // shows a + button that saves the current value
  onRemove?: (swatch: Swatch) => void; // remove button on hover, Delete on a focused swatch
  onReorder?: (swatches: Swatch[]) => void; // drag (or Alt + arrows) to reorder
  onRename?: (swatch: Swatch, label: string) => void; // double-click or F2
  renderSwatch?: (swatch: Swatch, state: { selected: boolean }) => ReactNode;
  emptyText?: ReactNode; // shown when the group is empty
}

ColorField props

Every ColorPicker prop, plus:

PropTypeWhat it does
labelstringAccessible name of the trigger, e.g. "Fill"
placeholderstringText when the value is empty
placement"bottom-start" | "bottom-end" | "top-start" | "top-end" | "right-start" | "left-start"Preferred side (default bottom-start). It flips and shifts to stay on screen
offsetnumberGap between the trigger and the picker in px (default 6)
open, defaultOpen, onOpenChangeboolean, (open) => voidControl the popover, or leave it uncontrolled
portalboolean | HTMLElement | nullRender in a portal on document.body (default) or inside an element; false renders inline
disabledbooleanDisable the trigger
renderTrigger({ value, open }) => ReactNodeYour own trigger contents
pickerClassName, pickerStylestring, CSSPropertiesStyle the picker inside the popover

A ref gives you { open(), close(), trigger }.

Parts

For your own layout, compose parts under Picker.Root (it takes the value props above):

PartWhat it is
RootThe panel and the state every other part reads
AreaThe 2D color area
Hue, AlphaHue and opacity sliders
ModeTabsSolid, Linear, Radial and Conic tabs
GradientEditorBar plus controls, shown only in gradient modes
GradientBarThe stop bar on its own
GradientControls, AngleDial, CenterPadAngle, shape, size and center controls
InputsFormat menu and channel fields
EyeDropperScreen color picking where the browser supports it
SwatchesSwatch groups with tabs, search, add and remove
CurrentSwatch, PreviewThe current value, with an optional before and after
ContrastWCAG contrast badge against a background
NoticeShown when a value cannot be read
import { Picker } from "@orshot/colorshot/react";

<Picker.Root value={value} onChange={setValue}>
  <Picker.Area />
  <Picker.Hue />
  <Picker.Alpha />
  <Picker.Inputs />
  <Picker.Swatches groups={groups} />
</Picker.Root>;

Theming

Set CSS variables on any parent or on the picker. Styles live in @layer colorshot, so your own unlayered CSS (Tailwind utilities included) wins without !important.

VariableWhat it sets
--cs-accentFocus rings and selected states
--cs-bg, --cs-fgPanel background and text
--cs-border, --cs-dividerBorders and dividers
--cs-radius, --cs-radius-controlPanel and control corners
--cs-width, --cs-padding, --cs-gapPanel size and spacing
--cs-area-heightHeight of the color area
--cs-swatch-columnsSwatches per row
--cs-shadowPanel shadow
--cs-font-sizeBase text size
--cs-motionTransition length (motion is off under prefers-reduced-motion)
.inspector [data-colorshot] {
  --cs-accent: #3e5ceb;
  --cs-width: 100%;
  --cs-area-height: 160px;
}

The documented --cs-* variables, data-part names and the data-state, data-mode, data-theme, data-size and data-variant attributes follow semver.

Localization

Every visible and screen reader string is a label. {placeholders} are filled in for you.

<ColorPicker
  value={value}
  onChange={setValue}
  labels={{
    picker: "Farbwähler",
    solid: "Einfarbig",
    linear: "Linear",
    radial: "Radial",
    conic: "Konisch",
    hue: "Farbton",
    alpha: "Deckkraft",
    stopName: "Farbstopp {index} von {count}",
  }}
/>

Keyboard and gestures

InputWhat it does
Two-finger swipeMove the area, sliders, stops and angle on a trackpad
Arrow keysNudge the focused thumb, stop or field (Shift for 10x)
1 to 4Switch Solid, Linear, Radial and Conic (picker focused)
IOpen the eyedropper
Cmd/Ctrl+C, Cmd/Ctrl+VCopy the value, or paste any CSS color or gradient
Cmd/Ctrl+Z, Shift+Cmd/Ctrl+ZUndo and redo, with the history prop
Click the bar, EnterAdd a stop
Alt + drag a stopCopy the stop
Drag off the bar, DeleteRemove a stop
Drag a swatch onto the barAdd it as a stop
EscapeDrop what you typed, or close a ColorField

Core helpers

@orshot/colorshot has no framework code and is safe in server code.

HelperWhat it does
createPicker(options)The framework-free store the React and Vue pickers use
parseColor(css)Any CSS color to { color, format }, or null. Never throws
formatColor(color, format, style?)Write a color as hex, rgb, hsl, oklch, p3 and more
convert, toSrgb, inGamut, toGamutColor space conversion and gamut mapping
isGradient, parseGradient, serializeGradientRead and write CSS gradients, keeping what you did not edit
gradientAngle, gradientCenterRead a gradient's angle and center
gradientHandles, moveGradientHandleStart, end, stop and center handles for your own canvas
contrastRatio(fg, bg), contrastLevel(ratio), luminanceWCAG contrast and its AA or AAA level
isSafeCssValue, safeCssValueOnly plain colors and gradients, before a stored value reaches a style
import { parseColor, formatColor, contrastRatio, isSafeCssValue } from "@orshot/colorshot";

formatColor(parseColor("#3366ff")!.color, "oklch"); // "oklch(0.5726 0.2338 265.28)"
contrastRatio("#64748b", "#ffffff"); // 4.76
isSafeCssValue(storedValue); // validate stored values before writing them into HTML or CSS

TypeScript

Types ship with the package, for every entry.

import type { ColorPickerProps, ColorFieldProps, SwatchGroupConfig, Swatch, DisplayFormat } from "@orshot/colorshot/react";
import type { OutputFormat, PickerMode, Color } from "@orshot/colorshot";

Next.js and SSR

The React entry is marked "use client": render the components from client components, and import helpers from @orshot/colorshot in server code. The picker renders the same markup on the server and the client. Pass theme from your theme provider after hydration, or leave it unset to follow the OS.

Accessibility

  • Checked with axe against WCAG 2.2 AA in the browser test suite
  • Every slider, stop and field is keyboard reachable, and sliders announce their value in words through aria-valuetext
  • Screen reader labels for every control, all overridable through labels
  • High contrast (forced-colors), right-to-left layouts and prefers-reduced-motion are supported
  • Touch screens get larger thumbs and targets

Browser support

Current Chrome, Edge, Firefox and Safari. The browser tests run in Chromium, Firefox and WebKit. The eyedropper uses the native EyeDropper API where it exists (Chromium) and hides itself elsewhere, unless you pass eyeDropper={fn}.

Development

pnpm install
pnpm --filter @colorshot/playground dev       # playground on http://localhost:5190
pnpm --filter @colorshot/core test            # parser, conversion and gradient corpus tests
pnpm --filter @colorshot/playground test:e2e  # browser tests on Chromium, Firefox and WebKit
pnpm build && pnpm size                       # build everything and check gzip budgets

See CONTRIBUTING.md for the layout and the rules, and RELEASING.md for publishing.

License

MIT. Free for personal and commercial use: keep the copyright line and license text, which link to https://orshot.com/open-source/colorshot. The color conversion matrices come from CSS Color Module Level 4, see THIRD-PARTY-NOTICES.md.


Brought to you by Orshot, the API for automated image, PDF and video generation from templates. MIT License.

color-picker
gradient
gradient-picker
oklch
react
react-color-picker
shadcn
typescript
vue
vue-color-picker

Languages

TypeScript

85.0%

CSS

12.4%

JavaScript

1.7%