Every project eventually needs a color picker. For years my default answer was <input type="color">, and every year I hit the same wall: it looks different in every browser, it gives you almost no control over the panel, and it is basically impossible to extend.
The usual alternative is a large UI library where the color picker is one component among hundreds. You install the whole thing, inherit its styling opinions, and hope the one prop you need actually exists.
There is a third option: treat the picker as a set of composable parts. That is the idea behind ew-color-picker.
The real problem is integration, not color math
Most color picker libraries already handle color math correctly. HSV, HSL, RGB, alpha, hex parsing, drag interactions — that part is solved.
What is not solved is how the picker fits into your product:
- Does it force a framework runtime?
- Can you remove the parts you do not need?
- Can you replace the saturation panel, buttons, or input without forking the library?
- Can it inherit your design system instead of fighting it?
- Can several pickers coexist on one page without global state collisions?
- Can it live inside a dialog or scroll container without being clipped?
ew-color-picker is built around these integration problems. The core keeps the current color and runs an event bus. The visible pieces are plugins: box, saturation panel, hue slider, alpha slider, input, numeric inputs, action buttons, preset swatches, and color-mode switcher.
That means you can use the all-in-one entry package, or import only the pieces you actually need.
What it solves, concretely
1. Composition at the package level
The all-in-one entry is a thin aggregator. Plugins ship as separate packages, so the picker is assembled from parts instead of only being toggled on and off with boolean flags.
If you do not need presets, do not register the preset plugin. If you want your own action row, skip the button plugin and render your own.
2. Framework independence
The picker works with plain DOM and events. There is no React, Vue, or Angular runtime inside it. A React component is a thin wrapper around new ewColorPicker(...), not a separate implementation.
That also means the same picker can be used in a design-system package, a CMS, or a white-label widget without rewriting it for each framework.
3. Design-system fit
Two options matter when the picker is embedded rather than used as a standalone widget:
-
prefixchanges every generated CSS class name, so the picker does not collide with another library that happens to use similar class names. -
appendTomounts the panel intodocument.bodyor any container, so dialogs, drawers, andoverflow: hiddencontainers do not clip it.
The SCSS build exposes a matching $prefix variable, and panel dimensions are driven by the --panel-width / --panel-height CSS variables rather than hard-coded viewport rules.
4. Predictable state and events
setColor() is the public entry point for programmatic color changes, and every change flows through the same event bus:
picker.on('change', (color) => { /* color changed */ }) picker.on('sure', (color, instance) => { /* apply clicked */ }) picker.on('toggle', (isOpen) => { /* panel opened or closed */ }) For multi-instance pages, each instance owns its own DOM, plugin instances, and event bus. There is no shared global color state.
5. TypeScript without a framework
The source is TypeScript, declarations ship with every JavaScript package, and the constructor options are typed. You get autocomplete for options without adopting a framework-specific wrapper. Event names are plain strings, so they are validated at runtime rather than through a typed event map.
Where it is useful
- Design systems and theme editors. Styles can be overridden with your own CSS, and the published SCSS sources can be recompiled with a custom
$prefix; panel size is driven by CSS variables. - Admin panels and CMS content editing. Presets, hex/RGB/HSL modes, alpha, clear, and apply are all available out of the box.
- White-label products and embedded widgets.
prefixandappendTosolve the two most common embedding problems: class collisions and panel clipping. - Canvas, drawing, and data-visualization tools. The picker is a small part of a larger editor; the event bus and
setColor()make it easy to treat as a control, not an application. - Pages with several pickers. Each instance is independent, so multiple color fields do not interfere with each other.
How it compares
There is no single best color picker. The right choice depends on what you want to customize.
| Option | Framework | Composition | Main strength | Best fit |
|---|---|---|---|---|
<input type="color"> | None | Zero JavaScript | Built into the browser | Quick hex-only input with no design requirements |
| UI-kit picker (Mantine, PrimeReact, ...) | Coupled to the kit | Whole library | Form integration and a consistent look | Apps already standardized on that UI kit |
| react-colorful | React only | One small component per layout | Tiny, dependency-free and fast | React popovers that need a standard layout |
| react-color | React only | One component per picker style | Many ready-made picker layouts | React apps that want Sketch/Photoshop-style components |
| Pickr | None | Single package with themes and built-in extensions | Feature-rich standalone picker | A complete picker with less assembly |
| iro.js | None | Single package | Wheel/slider style interactions | Creative tools that like the iro interaction model |
| coloris | None | Single package plus CSS | Minimal footprint and CSS-variable theming | Lightweight, simple color fields |
| ew-color-picker | None | Core plus per-plugin packages | Remove or swap out individual picker parts | Design systems, embedded widgets, and custom products |
The important difference is where customization happens:
- Native input: you customize almost nothing.
- UI kits: you customize through the kit's theme and props, but you take the whole kit.
- react-colorful and react-color: you choose among ready-made components.
- Pickr, iro.js, coloris: you configure and theme a complete picker.
- ew-color-picker: you choose which parts exist in the first place, and you can swap any of them for your own plugin.
This is not only about bundle size. It is about ownership. If your product needs a non-standard action row, a custom preset layout, or a panel that mounts outside a dialog, writing your own plugin is often simpler than overriding a monolith.
Honest trade-offs
ew-color-picker is newer and has a smaller ecosystem than react-colorful, react-color, Pickr, or a major UI kit. There are no official framework binding packages; the documentation shows thin integration patterns instead. It also has no gradient editor, multi-color editing, or eyedropper — those need a dedicated tool. If you want a fully themed, zero-configuration picker maintained by a large team, a UI-kit component is a reasonable choice.
The picker also does not pretend to be a complete design system. It gives you a small, typed, event-driven control that is designed to be embedded.
Try it in under a minute
npm install @ew-color-picker/ew-color-picker @ew-color-picker/style import ewColorPicker from '@ew-color-picker/ew-color-picker' import '@ew-color-picker/style/dist/index.css' import '@ew-color-picker/style/dist/input-number.css' const picker = new ewColorPicker({ el: '#color-picker', defaultColor: '#6366f1', predefineColor: ['#ef4444', '#22c55e', '#3b82f6', '#0f172a'], }) picker.on('change', (color) => { document.documentElement.style.setProperty('--brand-color', color) }) Both stylesheets are required: index.css styles the picker, and input-number.css styles the numeric inputs used by RGB and HSL modes.
If you prefer a shorter package name, npm install ew-color-picker installs a thin compatibility entry that re-exports the same API.
If you want to try it without a build step, the UMD bundle works directly from a CDN:
<link rel="stylesheet" href="https://unpkg.com/@ew-color-picker/style/dist/index.css" /> <link rel="stylesheet" href="https://unpkg.com/@ew-color-picker/style/dist/input-number.css" /> <div id="color-picker"></div> <script src="https://unpkg.com/@ew-color-picker/ew-color-picker/dist/index.min.js"></script> <script> new ewColorPicker({ el: '#color-picker', defaultColor: '#ff0000' }) </script> Assemble only the parts you need
If you only want a color box, a saturation panel, a hue slider, a text input, and the action buttons, import exactly those:
import ewColorPicker from '@ew-color-picker/core' import Box from '@ew-color-picker/box' import Panel from '@ew-color-picker/panel' import Hue from '@ew-color-picker/hue' import Input from '@ew-color-picker/input' import Button from '@ew-color-picker/button' ewColorPicker.use(Box) .use(Panel) .use(Hue) .use(Input) .use(Button) new ewColorPicker({ el: '#color-picker' }) use() is a static registration step. Each plugin declares a pluginName, receives the picker instance, and can render into a mount point or subscribe to color changes. Registration is deduplicated by pluginName, so repeated imports or HMR do not register the same plugin twice.
Writing a custom plugin
A plugin follows the same shape as the built-ins:
import ewColorPicker from '@ew-color-picker/core' class BrandSwatchesPlugin { static pluginName = 'brandSwatches' constructor(picker: ewColorPicker) { const host = picker.getMountPoint('panelContainer') // render your own UI into `host` // subscribe to `change` when the color updates picker.on('change', (color) => { // update your UI }) } } ewColorPicker.use(BrandSwatchesPlugin) Because the plugin receives the picker instance, it can call the same public API as application code: getColor(), setColor(), getOptions(), showPanel(), and so on.
Embedding it in a design system
When the picker lives inside another library or a white-label product, configure both sides consistently:
new ewColorPicker({ el: '#color-picker', prefix: 'acme-', appendTo: document.body, panelPlacement: 'bottom-start', }) @use '@ew-color-picker/style/src/index.scss' with ($prefix: 'acme-'); @use '@ew-color-picker/style/src/input-number.scss' with ($prefix: 'acme-'); The JS prefix and SCSS $prefix must match. The prebuilt dist CSS uses ew-; custom prefixes require compiling the SCSS source.
Practical options, not a configuration maze
Most options map directly to a product requirement:
new ewColorPicker({ el: '#color-picker', defaultColor: 'rgba(99, 102, 241, 0.8)', size: 'large', hueDirection: 'horizontal', alphaDirection: 'horizontal', predefineColor: [ '#ef4444', '#22c55e', '#3b82f6', { color: '#0f172a', disabled: true }, ], clearText: 'Reset', sureText: 'Apply', panelPlacement: 'bottom-start', togglePickerAnimation: 'height', pickerAnimationTime: 250, }) You can disable individual pieces with hasPanel, hasInput, showButton, showColorMode, and similar flags, or remove the hue and alpha sliders with hue: false and alpha: false. Hex, RGB, and HSL modes are supported, plus an optional alpha slider.
Using it in React
The picker does not need a React wrapper. The rules are: create it after mount, destroy it on unmount, and write external values through setColor() behind a guard so the update is not echoed back as a user change.
import { useEffect, useRef } from 'react' import ewColorPicker from '@ew-color-picker/ew-color-picker' import '@ew-color-picker/style/dist/index.css' import '@ew-color-picker/style/dist/input-number.css' interface Props { value: string onChange: (color: string) => void } export function ColorField({ value, onChange }: Props) { const hostRef = useRef<HTMLDivElement>(null) const pickerRef = useRef<ewColorPicker | null>(null) const lastEmitted = useRef(value) const syncing = useRef(false) useEffect(() => { const picker = new ewColorPicker({ el: hostRef.current!, defaultColor: value, changeColor: (color: string) => { if (syncing.current) return lastEmitted.current = color onChange(color) }, }) pickerRef.current = picker return () => { picker.destroy() pickerRef.current = null } }, []) // Controlled: external value -> picker, without echoing back through onChange useEffect(() => { const picker = pickerRef.current if (!picker || value === lastEmitted.current) return syncing.current = true picker.setColor(value) syncing.current = false lastEmitted.current = value }, [value]) return <div ref={hostRef} /> } Two details that are easy to miss:
- Prefer a DOM element over a selector string. With several instances, selectors can collide.
- Always call
destroy()on unmount. While the panel is open, the instance registers a document mousedown listener; leaking it can make other instances misbehave.
The documentation has the same minimal pattern for Vue 3 and Angular.
Using it in Solid
Solid needs the same lifecycle handling, and the wrapper is small enough that it is worth showing the real one I use. It imports the core plus only the plugins it needs — trigger box, saturation panel, hue slider and presets — rather than the all-in-one entry. The picker is created in onMount, destroyed in onCleanup, and an external value is written back through setColor() behind a re-entrancy guard.
import { createEffect, onCleanup, onMount } from 'solid-js' import EwColorPicker from '@ew-color-picker/core' import Box from '@ew-color-picker/box' import Panel from '@ew-color-picker/panel' import Hue from '@ew-color-picker/hue' import Predefine from '@ew-color-picker/predefine' import '@ew-color-picker/style/dist/index.css' EwColorPicker.use(Box).use(Panel).use(Hue).use(Predefine) interface Props { value: string presets?: string[] onChange: (color: string) => void } export function ColorField(props: Props) { let host!: HTMLDivElement let picker: EwColorPicker | undefined let lastEmitted = props.value let syncing = false onMount(() => { picker = new EwColorPicker({ el: host, defaultColor: props.value, predefineColor: props.presets ?? [], changeColor: (color) => { if (syncing) return lastEmitted = color props.onChange(color) }, }) }) // Controlled: external value -> picker, without echoing back through onChange createEffect(() => { const value = props.value if (!picker || value === lastEmitted) return syncing = true picker.setColor(value) syncing = false lastEmitted = value }) onCleanup(() => picker?.destroy()) return <div ref={host} /> } Because a plugin receives the picker instance and a DOM mount point, you can also render Solid JSX straight into the picker chrome. In the example above I mount the app's own input and button inside the picker's bottom row, without forking the library:
import { render } from 'solid-js/web' import { createSignal, onCleanup, onMount } from 'solid-js' import { ApplyOrder } from '@ew-color-picker/utils' function AppControls(props: { picker: EwColorPicker }) { const [color, setColor] = createSignal(props.picker.getColor() || '') onMount(() => { const sync = (value: string) => setColor(value) props.picker.on('change', sync) onCleanup(() => props.picker.off('change', sync)) }) return ( <div class="app-controls"> <input value={color()} onChange={(e) => props.picker.setColor(e.currentTarget.value)} /> <button onClick={() => props.picker.hidePanel()}>Done</button> </div> ) } class AppControlsPlugin { static pluginName = 'appControls' // Run after the panel plugin has created its bottom row static applyOrder = ApplyOrder.Post private dispose?: () => void constructor(picker: EwColorPicker) { const panel = picker.getMountPoint('panelContainer') const bottomRow = panel?.querySelector('.ew-color-picker-bottom-row') if (bottomRow) { this.dispose = render(() => <AppControls picker={picker} />, bottomRow) } } destroy() { this.dispose?.() } } EwColorPicker.use(AppControlsPlugin) use() deduplicates by pluginName, so HMR or multiple entry points do not register the plugin twice. destroy() is called by the core when the picker is destroyed, so the Solid render root is disposed together with the instance.
The selector above assumes the default ew- prefix. If you customize prefix, build the bottom-row selector from getPrefix(picker) (exported by @ew-color-picker/utils) so it matches the generated class name.
One extra trick from the same wrapper: if the host app already has its own .ew-color-picker styles, you can scope the imported library stylesheet to your own subtree (Vite's ?raw import) so the library's rules do not leak into other components:
import pickerCss from '@ew-color-picker/style/dist/index.css?raw' const style = document.createElement('style') style.textContent = `@scope (.color-field-scope) {\n${pickerCss}\n}` document.head.appendChild(style) This does not override host rules that target the picker inside your subtree, but it stops the library from restyling anything outside it. No !important and no forced prefix change.
When I would not use it
- You only need a simple hex input and do not care about browser differences:
<input type="color">is enough. - You are already standardized on a UI kit and need its form, theme, and accessibility behavior more than you need picker-level control.
- You want an official framework binding package with zero integration code.
- You need a feature-rich standalone picker with almost no assembly: Pickr or a UI-kit component may fit better.
The picker is intentionally a control, not a complete application.
Links
- Documentation: https://eveningwater.github.io/ew-color-picker/
- Live demo: https://eveningwater.github.io/ew-color-picker/examples/demo
- GitHub: https://github.com/eveningwater/ew-color-picker
- npm: https://www.npmjs.com/package/@ew-color-picker/ew-color-picker
If the plugin model is useful to you, a star on GitHub helps more people find it. And if you build a plugin that fills a gap, I would genuinely like to see it.

