A lightweight, dependency-free color picker for text inputs. The panel is a helper — never a replacement for the field.
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.
// Per Attribut, ohne einen eigenen Aufruf je Feld:
<input type="text" data-pp-thumb="circle" value="#06d6a0">
// …oder fuer viele Felder auf einmal:
create('.color-input', { thumbStyle: 'circle' });
// Die Seite waehlen -- 'start' folgt der Schreibrichtung,
// steht also in einem RTL-Layout rechts.
create('.color-input', {
thumbStyle: 'circle',
thumbPosition: 'start'
});
// Die Schalter oben bauen die Instanz jeweils neu auf:
destroy(field);
create(field, { thumbStyle: style, thumbPosition: 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.
create('#demo-format', { formatToggle: true });
create('#demo-swatches', { swatchesOnly: true });
create('#demo-noalpha', { alpha: false });
create('#demo-pill', { theme: 'pill' });
create('#demo-dark', { themeMode: 'dark' });
create('#demo-clear', { clearButton: true });
// Eigene Farbfelder statt der zwoelf Standardwerte:
create('#brand-color', {
swatches: ['#4361ee', '#7209b7', '#f72585', '#ffbe0b'],
swatchesOnly: true
});
formatToggle: true — switch between hex, rgb and hsl.
swatchesOnly: true — a fixed palette, nothing else.
alpha: false — opaque colors only.
theme: 'pill' — rounded panel and swatches.
themeMode: 'dark' — regardless of the OS setting.
clearButton: true — empties the field.
The field stays yours: type a value straight into it and the panel follows along. From code, every instance can be created, read, set and taken apart again.
Click and type #f72585, rebeccapurple or rgb(42 157 143) — the focus never leaves the input.
focus() on its own internal input when the field was clicked, so anything the user typed went into the panel instead of the field. Here the field keeps the focus and the caret; the panel only ever reads from it.
After destroy() the field is indistinguishable from one that never had a picker — no listeners, no wrapper, no leftover nodes.
// Tippen im Feld melden -- onInput faellt bei jeder Aenderung,
// auch bei der von Hand eingegebenen.
create('#demo-typing', {
onInput: (value) => console.log(value)
});
const [picker] = create('#demo-lifecycle');
picker.setValue('#d62828'); // Wert von aussen setzen
picker.getValue(); // '#d62828'
picker.open(); // Panel oeffnen
picker.close({ revert: true }); // schliessen, Wert zuruecknehmen
getAll().length; // wie viele Instanzen leben
picker.destroy(); // diese eine abbauen
destroy('#demo-lifecycle'); // …oder ueber das Feld
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.
Positioned inside the box, the usual way.
…and the rest is cut off.
Same box, same overflow: hidden.
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.
import { create } from 'pixelpicker';
import 'pixelpicker/css';
create('.color-input', {
thumbStyle: 'circle',
formatToggle: true
});
<link rel="stylesheet" href="dist/pixelpicker.css">
<script src="dist/pixelpicker.js"></script>
<script>
PixelPicker.create('.color-input', { thumbStyle: 'bar' });
</script>
<input type="text" data-pixelpicker data-pp-thumb="circle" value="#2a9d8f">
<script>PixelPicker.auto();</script>
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.
Everything you can pass to create(). Each option is independent — set only the ones you want to change.
| Option | Type | Default | Description |
|---|---|---|---|
thumbStyle | string | '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. |
thumbPosition | string | '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. |
wrap | string | 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. |
theme | string | 'default' |
Shape of the panel: 'default' with regular corners, or 'pill' with rounded controls and round swatches. |
themeMode | string | 'auto' |
Color scheme of the panel: 'light', 'dark', or 'auto' to follow the operating system setting. |
| Option | Type | Default | Description |
|---|---|---|---|
openOn | string | 'click' |
What opens the panel: 'click' on the field, 'focus' (so it also opens via keyboard), or 'manual' to open it yourself with open(). |
closeOnSwatch | boolean | false |
Set true to close the panel as soon as a swatch is picked. Useful together with swatchesOnly, where one click is the whole interaction. |
returnFocus | boolean | true |
Put the focus back into the field when the panel closes. Set false if your own code moves the focus somewhere else. |
gap | number | 4 |
Distance in pixels between the field and the panel. |
forcePositionFallback | boolean | false |
Set true to use the JavaScript positioning even in browsers that support CSS Anchor Positioning. For testing that path — leave it off in production. |
| Option | Type | Default | Description |
|---|---|---|---|
format | string | 'auto' |
What gets written into the field: 'hex', 'rgb', 'hsl', 'mixed', or 'auto' to keep whatever format the field already holds. |
formats | array | ['hex','rgb','hsl'] |
Which formats the switcher offers, in this order. Shorten it to offer fewer. |
formatToggle | boolean | false |
Set true to show the format switcher inside the panel. |
alpha | boolean | true |
Show the opacity slider. Set false for opaque colors only. |
forceAlpha | boolean | false |
Set true to always write the alpha channel, even when it is 1 — you get #2a9d8fff instead of #2a9d8f. |
defaultColor | string | '#000000' |
Which color the panel starts on when the field is empty. |
swatches | array | 12 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. |
swatchesOnly | boolean | false |
Set true to show nothing but the palette — no area, no sliders. For brand colors, where free choice is not wanted. |
| Option | Type | Default | Description |
|---|---|---|---|
clearButton | boolean | false |
Set true to add a button that empties the field. |
closeButton | boolean | true |
The button that closes the panel. Set false to rely on clicking outside or pressing Esc. |
labels | object | English | Every visible and assistive string, for translating the panel. Keys you leave out keep their English default — see Localization. |
| Option | Type | Default | Description |
|---|---|---|---|
onInput | function | null |
(value, instance) => void — fires on every change, including mid-drag and while typing into the field. |
onChange | function | null |
(value, instance) => void — fires when a selection is complete. This is the one to save a value on. |
onOpen | function | null |
(value, instance) => void — fires after the panel opens. |
onClose | function | null |
(value, instance) => void — fires after the panel closes. |
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.
| Option | Attribute |
|---|---|
thumbStyle | data-pp-thumb |
thumbPosition | data-pp-thumb-position |
wrap | data-pp-wrap |
theme | data-pp-theme |
themeMode | data-pp-theme-mode |
openOn | data-pp-open-on |
closeOnSwatch | data-pp-close-on-swatch |
gap | data-pp-gap |
format | data-pp-format |
formatToggle | data-pp-format-toggle |
alpha | data-pp-alpha |
forceAlpha | data-pp-force-alpha |
defaultColor | data-pp-default-color |
swatches | data-pp-swatches — comma-separated |
swatchesOnly | data-pp-swatches-only |
clearButton | data-pp-clear-button |
closeButton | data-pp-close-button |
formats, labels, returnFocus, forcePositionFallback and the callbacks have no attribute — they take values that do not fit into one.
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.
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
});
| Event | When |
|---|---|
pixelpicker:open | The panel opened |
pixelpicker:input | The value changed, possibly mid-drag |
pixelpicker:change | A selection completed |
pixelpicker:close | The panel closed |
| Function | Returns | Meaning |
|---|---|---|
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?) | number | Tear down; without an argument, all |
closeAll() | void | Close every open panel |
auto(selector?, options?) | PickerInstance[] | Pick up [data-pixelpicker] fields |
| Method | Meaning |
|---|---|
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 |
The whole picker is operable without a mouse. The field behaves like any other text input until you deliberately step into the panel.
| Key | Where | What 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. |
Esc | Anywhere | 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. |
Tab | In the panel | Steps through the controls: saturation area, hue, opacity, the value field, the format switcher, the swatches. |
| Arrow keys | On the saturation area | Move the marker one step at a time — left and right change saturation, up and down change brightness. |
Shift + arrows | On the saturation area | The same, in steps of ten, for crossing the area quickly. |
| Arrow keys | On a slider | Adjust hue or opacity. These are native range inputs, so they behave exactly as the browser's own do. |