pixelpicker

A lightweight, dependency-free color picker for text inputs. The panel is a helper — never a replacement for the field.

No jQuery Zero dependencies ESM · CJS · IIFE ~20 KB minified Popover API: checking… Anchor Positioning: checking… MIT License
Get started View on npm View on GitHub

Live demo

Every tab shows a working picker. Show setup code reveals exactly how it was configured.

How the chosen color appears in the input itself. Six variants via thumbStyle, each on either side via thumbPosition — click a field to open its picker, or switch the side.

thumbStyle: 'bar'
thumbPosition: 'end'

A strip at the edge of the field. The default.

thumbStyle: 'circle'
thumbPosition: 'end'

A round swatch inside the field.

thumbStyle: 'square'
thumbPosition: 'end'

A square swatch inside the field.

thumbStyle: 'fill'

The color covers the field completely — the value is hidden. For fields where the color is the display.

thumbStyle: 'fill-behind'

The color fills the field behind the text, so the value stays readable.

thumbStyle: 'none'

No swatch at all — the input is left exactly as it was.

It cannot be clipped or buried

Put an input inside a box with overflow: hidden and a conventional picker disappears the moment it opens — the box cuts it off. The panel here lives in the browser's top layer, above everything on the page, so no ancestor can clip it and no z-index can bury it. Open both fields below to see the difference.

A conventional panel

Positioned inside the box, the usual way.

pixelpicker

Same box, same overflow: hidden.

Scrolling, flipping, and the fallback

The panel keeps its place while the page or a container scrolls, and flips above the field when there is no room below. Where the browser supports CSS Anchor Positioning it does that itself; otherwise a small JavaScript fallback measures the field. Force the fallback to check both paths.

This container scrolls. Open a field, then scroll — the panel follows.

Using: checking…

Getting started

As a module

import { create } from 'pixelpicker';
import 'pixelpicker/css';

create('.color-input', {
  thumbStyle: 'circle',
  formatToggle: true
});

As a plain script

<link rel="stylesheet" href="dist/pixelpicker.css">
<script src="dist/pixelpicker.js"></script>
<script>
  PixelPicker.create('.color-input', { thumbStyle: 'bar' });
</script>

Without any JavaScript call

<input type="text" data-pixelpicker data-pp-thumb="circle" value="#2a9d8f">

<script>PixelPicker.auto();</script>

In an existing design

By default the picker needs an element to anchor the swatch to, because an <input> cannot hold children. It uses the input's parent when the input sits there alone, and only inserts a wrapper when it does not.

That last case breaks markup built on a direct parent-child or sibling relationship. Bootstrap's .form-floating is the common one: it expects the input and its label to be siblings, and an inserted element costs the field its height and shape. Set wrap: false and the existing parent takes the role instead.

<div class="form-floating">
  <input type="text" class="form-control" id="brand" value="#87a878">
  <label for="brand">Brand color</label>
</div>

<script>
  PixelPicker.create('#brand', { thumbStyle: 'circle', wrap: false });
</script>

The swatch scales with the field: circle and square take 60 % of its height, down to a floor of 14 px. Both are yours to change — set --pp-thumb-size and --pp-thumb-min-size on the field's parent.

Options

Everything you can pass to create(). Each option is independent — set only the ones you want to change.

Appearance

OptionTypeDefaultDescription
thumbStylestring'bar' How the color appears in the input. One of 'bar', 'circle', 'square', 'fill', 'fill-behind' or 'none'. 'fill' covers the value, 'fill-behind' keeps it readable.
thumbPositionstring'end' Which side the swatch sits on: 'end' or 'start'. It follows the writing direction, so 'start' is on the right in a right-to-left layout.
wrapstring | boolean'auto' Whether a wrapper element may be inserted around the input. 'auto' adds one only when the parent holds more than the input, false always uses the existing parent, true always inserts one. Use false for markup that relies on a direct parent-child or sibling relationship, such as Bootstrap's .form-floating.
themestring'default' Shape of the panel: 'default' with regular corners, or 'pill' with rounded controls and round swatches.
themeModestring'auto' Color scheme of the panel: 'light', 'dark', or 'auto' to follow the operating system setting.

Behaviour

OptionTypeDefaultDescription
openOnstring'click' What opens the panel: 'click' on the field, 'focus' (so it also opens via keyboard), or 'manual' to open it yourself with open().
closeOnSwatchbooleanfalse Set true to close the panel as soon as a swatch is picked. Useful together with swatchesOnly, where one click is the whole interaction.
returnFocusbooleantrue Put the focus back into the field when the panel closes. Set false if your own code moves the focus somewhere else.
gapnumber4 Distance in pixels between the field and the panel.
forcePositionFallbackbooleanfalse Set true to use the JavaScript positioning even in browsers that support CSS Anchor Positioning. For testing that path — leave it off in production.

Color

OptionTypeDefaultDescription
formatstring'auto' What gets written into the field: 'hex', 'rgb', 'hsl', 'mixed', or 'auto' to keep whatever format the field already holds.
formatsarray['hex','rgb','hsl'] Which formats the switcher offers, in this order. Shorten it to offer fewer.
formatTogglebooleanfalse Set true to show the format switcher inside the panel.
alphabooleantrue Show the opacity slider. Set false for opaque colors only.
forceAlphabooleanfalse Set true to always write the alpha channel, even when it is 1 — you get #2a9d8fff instead of #2a9d8f.
defaultColorstring'#000000' Which color the panel starts on when the field is empty.
swatchesarray12 colors The palette shown at the bottom of the panel. Pass your own array of color strings, or an empty array [] to hide the palette entirely.
swatchesOnlybooleanfalse Set true to show nothing but the palette — no area, no sliders. For brand colors, where free choice is not wanted.

Controls

OptionTypeDefaultDescription
clearButtonbooleanfalse Set true to add a button that empties the field.
closeButtonbooleantrue The button that closes the panel. Set false to rely on clicking outside or pressing Esc.
labelsobjectEnglish Every visible and assistive string, for translating the panel. Keys you leave out keep their English default — see Localization.

Callbacks

OptionTypeDefaultDescription
onInputfunctionnull (value, instance) => void — fires on every change, including mid-drag and while typing into the field.
onChangefunctionnull (value, instance) => void — fires when a selection is complete. This is the one to save a value on.
onOpenfunctionnull (value, instance) => void — fires after the panel opens.
onClosefunctionnull (value, instance) => void — fires after the panel closes.

The same options as data attributes

Most options can be set on the element itself, which is handy for server-rendered markup: no JavaScript call per field, just auto() once. An attribute beats the value passed to create(), because it is the more specific source.

OptionAttribute
thumbStyledata-pp-thumb
thumbPositiondata-pp-thumb-position
wrapdata-pp-wrap
themedata-pp-theme
themeModedata-pp-theme-mode
openOndata-pp-open-on
closeOnSwatchdata-pp-close-on-swatch
gapdata-pp-gap
formatdata-pp-format
formatToggledata-pp-format-toggle
alphadata-pp-alpha
forceAlphadata-pp-force-alpha
defaultColordata-pp-default-color
swatchesdata-pp-swatches — comma-separated
swatchesOnlydata-pp-swatches-only
clearButtondata-pp-clear-button
closeButtondata-pp-close-button

formats, labels, returnFocus, forcePositionFallback and the callbacks have no attribute — they take values that do not fit into one.

Many options on one field? Use data-pp-config with a JSON object instead of a long list of attributes: data-pp-config='{"thumbStyle":"circle","alpha":false}'. Individual attributes still take precedence over the JSON.

Events

Each instance dispatches events on its own field, so they bubble and can be delegated. Native input and change events fire as well — forms and validators see exactly what they would see from typing.

field.addEventListener('pixelpicker:change', (event) => {
  console.log(event.detail.value);     // '#2a9d8f'
  console.log(event.detail.instance);  // the PickerInstance
});
EventWhen
pixelpicker:openThe panel opened
pixelpicker:inputThe value changed, possibly mid-drag
pixelpicker:changeA selection completed
pixelpicker:closeThe panel closed

API

FunctionReturnsMeaning
create(target, options?)PickerInstance[]Attach to a selector, element or list
get(target)PickerInstance?The instance belonging to a field
getAll()PickerInstance[]Every live instance
destroy(target?)numberTear down; without an argument, all
closeAll()voidClose every open panel
auto(selector?, options?)PickerInstance[]Pick up [data-pixelpicker] fields

On an instance

MethodMeaning
open() / close() / toggle()Control the panel. close({ revert: true }) restores the value from before opening.
getValue()The field's current value
setValue(value, { silent })Set the value; silent suppresses the input event
destroy()Tear this instance down

Keyboard

The whole picker is operable without a mouse. The field behaves like any other text input until you deliberately step into the panel.

KeyWhereWhat happens
Alt + In the field Opens the panel and moves the focus into it. This is the only way the focus leaves the field, and it takes a deliberate press.
EscAnywhere Closes the panel and puts back the value the field had before it opened — an undo for a selection you did not mean to make.
TabIn the panel Steps through the controls: saturation area, hue, opacity, the value field, the format switcher, the swatches.
Arrow keysOn the saturation area Move the marker one step at a time — left and right change saturation, up and down change brightness.
Shift + arrowsOn the saturation area The same, in steps of ten, for crossing the area quickly.
Arrow keysOn a slider Adjust hue or opacity. These are native range inputs, so they behave exactly as the browser's own do.