Advanced Topics
Architecture
Section titled “Architecture”Signals-Based Reactivity
Section titled “Signals-Based Reactivity”The component uses Preact Signals for fine-grained reactivity. State changes update only the affected parts of the UI instead of causing a broad re-render of the whole component.
Shadow DOM
Section titled “Shadow DOM”Styles and internal markup are encapsulated in Shadow DOM. That keeps the picker stable when embedded in very different host apps while still exposing major styling hooks through ::part().
Area Picker
Section titled “Area Picker”The 2D color area is not a single generic gradient. It changes its model depending on the active editing space:
srgbandhexuse an OKHSV-style planehsluses saturation × lightnessoklch,lch, and wide-gamut RGB-like spaces use a chroma × lightness planeoklabandlabuse a centereda × bplanehwbuses whiteness × blackness
That choice makes the picker behave more naturally for different color models instead of forcing everything through one generic RGB-oriented interaction.
Gamut-aware rendering
Section titled “Gamut-aware rendering”For spaces where gamut boundaries are especially relevant, the area picker can render overlays for multiple gamuts and adjust its mapping so the useful region fills more of the control.
Examples:
oklch-style picking can show row-scanned chroma boundaries across lightnessoklabandlabcan show polar gamut boundaries across thea × bplane- wide-gamut RGB-like spaces are edited through a perceptual plane while still reflecting the chosen output space
This helps the picker communicate when a color fits within sRGB, P3, Rec2020, or beyond.
Color Math
Section titled “Color Math”Color calculations are powered by colorjs.io, which provides the parsing, conversion, gamut checks, and serialization logic behind the UI.
Performance
Section titled “Performance”The current implementation uses several practical performance strategies:
- Signals prevent unnecessary UI work
- hue-driven area updates are batched with
requestAnimationFrame - the area gradient is drawn into canvas rather than constructed from many DOM nodes
- wide-gamut canvas rendering is used when supported and falls back to sRGB otherwise
- a worker can be used to move expensive area and boundary computation off the main thread
- boundary overlays are drawn separately from the underlying gradient
The worker-backed path is especially useful for perceptual and wide-gamut modes, where generating the area and boundary data involves many color-space conversions.
Popover Positioning
Section titled “Popover Positioning”The popover automatically positions relative to the trigger button, respects viewport boundaries, and accounts for safe-area constraints.
The implementation has two positioning modes:
- native CSS anchor positioning when supported and when the internal trigger is the anchor
- a manual fixed-position fallback when a custom anchor is used or anchor positioning is unavailable
The fallback mode observes relevant layout changes and clamps the panel to the visible viewport.
For custom placement, use setAnchor():
const picker = document.querySelector("color-input");picker.setAnchor(document.querySelector("#my-anchor"));picker.show();Gamut Detection
Section titled “Gamut Detection”The read-only gamut property reports the smallest tracked gamut that contains the current color:
srgb— safe for standard displaysp3— requires Display P3 or betterrec2020— requires a wider display pipelinexyz— outside the named RGB gamuts tracked by the picker
A visual badge appears in the UI, and the change event includes event.detail.gamut for programmatic use.
Value flow and normalization
Section titled “Value flow and normalization”From a developer’s perspective, the component typically does this when values change:
- parse the incoming CSS color string
- infer or apply the active editing colorspace
- convert the color for the relevant controls
- serialize a normalized string back to
value - emit
changewith the latestvalue,colorspace, andgamut
That means value is best treated as the component’s canonical serialized output, not necessarily a byte-for-byte preservation of the original source string.
Progressive enhancement
Section titled “Progressive enhancement”Several parts of the component are intentionally opportunistic:
- EyeDropper controls appear only in supporting browsers
- wide-gamut canvas rendering is used only when supported by the browser’s canvas implementation
- native anchor positioning is used only when available
- worker-backed area rendering falls back to synchronous rendering if needed
The goal is to improve the experience in modern browsers without making the component unusable elsewhere.
Form Integration
Section titled “Form Integration”The component does not extend HTMLInputElement, but it integrates easily with forms through a hidden input:
<form> <color-input id="picker" value="oklch(70% 20% 240)"></color-input> <input type="hidden" name="color" id="color-input" /></form>
<script> const picker = document.querySelector("#picker"); const hidden = document.querySelector("#color-input");
hidden.value = picker.value; picker.addEventListener("change", (event) => { hidden.value = event.detail.value; });</script>