-
Notifications
You must be signed in to change notification settings - Fork 10
Color picker
- Color picker specification
Team Name: Design and Web Development
Developer name: Radoslav Karaivanov
Designer name:
| Version | Author | Date | Description |
|---|---|---|---|
| 1 | Radoslav Karaivanov | 2026-06-22 | Initial draft |
| 2 | Radoslav Karaivanov | 2026-07-31 | Updated to match the implemented component behavior. |
The igc-color-picker component is a form-associated control that lets users pick and edit a color
through an interactive dropdown. It supports two anchor presentations: mode="default" renders a
trigger button that previews the current color (a checkered pattern when no value is set), while
mode="input" renders an editable text field with a color swatch prefix. Activating the anchor
opens a picker surface that contains an HSV saturation/value gradient canvas, a hue slider, an
optional alpha slider, a format-aware color string input, an optional eye-dropper, a
copy-to-clipboard action and an optional grid of predefined swatches. The selected color is exposed
as a string in one of three formats (hex, rgb, hsl), can be cleared to an empty value, and
participates in standard HTML form submission, required validation and validation-message
projection.
The igc-color-picker must:
- be WAI-ARIA compliant.
- participate in form association (submit its
valueunder itsname, support reset,requiredand custom validation through the form-associated lifecycle). - parse and accept any valid CSS color string (hex, rgb/rgba, hsl/hsla and named colors) assigned to
value; an empty, whitespace-only or invalid string clears the value. - render a checkered anchor background while the value is empty.
- serialize its
valueconsistently in the currently selectedformat. - keep the gradient canvas marker, hue slider, alpha slider and color value input synchronized with the current color at all times, deriving the canvas position from HSV saturation/value.
- allow switching between
hex,rgbandhslformats without losing the represented color. - optionally hide the format switcher via
hide-formatsand show/hide the alpha slider viashow-alpha. - render an eye-dropper action only when the platform
EyeDropperAPI is available. - copy the current color value to the clipboard on demand.
- render a predefined set of color swatches when supplied and apply a swatch on selection.
- close on outside click,
EscapeandAlt+ArrowUp, and open onAlt+ArrowDown, restoring focus to the anchor. - emit
igcInputon every interim interaction andigcChangeonce, when the committed value has changed and focus leaves the component entirely. - project validation messages (
value-missing,custom-error,invalid,helper-text) and reference them from the anchor viaaria-describedby. - expose styling hooks for the slider tracks and the current color through CSS custom properties.
As an end-user I expect to be able to:
- open the picker from a labelled anchor - a button previewing the current color, or an editable field with a color swatch - that shows a checkered pattern when no color is set.
- drag inside the gradient area to adjust saturation and brightness.
- drag the hue slider to change the base color and the alpha slider (when shown) to change opacity.
- type an exact color string (hex, rgb(a), hsl(a) or a named color) into the color value input.
- clear the value by deleting the text of the color value input (or the anchor input, in
inputmode). - switch between hex, RGB and HSL representations from a dropdown.
- pick a color from anywhere on my screen using the eye-dropper (where supported).
- copy the selected color value to my clipboard.
- choose from a set of predefined swatches when they are provided.
- dismiss the picker with
Escape,Alt+ArrowUpor by clicking outside of it. - see a validation message when the field is required and left empty.
As a developer I expect to be able to:
- bind the picker to a form by setting
nameand read/write itsvalueas a string. - choose the anchor presentation via
mode("default"or"input"). - choose the serialization
format(hex,rgborhsl) of the emitted value. - provide a list of predefined
swatches. - hide the format switcher with
hide-formatsand show the alpha slider withshow-alpha. - mark the control as
required, disable it, and surface validation state through the standard form-associated API (checkValidity,reportValidity,setCustomValidity,invalid) and thevalue-missing/custom-error/invalid/helper-textslots. - programmatically open, close or toggle the picker.
- react to interim and committed value changes through
igcInputandigcChange, and toigcOpening,igcOpened,igcClosingandigcClosed. - customize the hue/alpha slider tracks and the current color through CSS custom properties.
Developers place an igc-color-picker in markup and optionally bind it to a form via the name
attribute. In mode="default" (the default) the anchor is a trigger button previewing the current
color; in mode="input" the anchor is an editable igc-input with a color swatch prefix that also
opens the picker. Both anchors reference the validation/helper-text container via
aria-describedby="color-picker-helper-text". Clicking the anchor toggles the picker dropdown,
which is positioned by an internal igc-popover (with flip and shift enabled).
The value property accepts any valid CSS color string. On assignment, the string is parsed and
validated (isValidColor) into an internal ColorModel that maintains synchronized RGB, HSL and HSV
representations plus an alpha channel. An empty, whitespace-only or invalid string produces an
"empty" ColorModel instead of a stale color; while empty, the anchor renders a checkered
background (part="anchor empty") and the serialized value is ''. The active format
determines how the color is re-serialized into the externally observable value.
Inside the picker the user can:
- Drag within the
igc-picker-canvasgradient to set HSV saturation and value. - Drag the
huerange input to set hue. - Drag the
alpharange input (rendered whenshow-alphais set) to set opacity, or type an exact alpha value into the accompanying numeric input. - Type a full color string directly into the color value input; it is re-serialized in the active
format. A non-empty but invalid string reverts the input to the last valid color; an empty string clears the value.
Every interaction routes through a single _updateColor() path that updates the --current-color
custom property, re-serializes the form value, re-runs validation, and emits igcInput. The
component tracks the value at the moment focus enters it and compares it to the value once focus
leaves entirely (focusin/focusout), emitting igcChange exactly once if it changed - dragging
the canvas, sliders, or typing never emits igcChange by itself. After each change and on open, the
canvas marker position is re-synchronized with the current color's HSV saturation/value on the next
animation frame.
The format attribute selects the serialization used by the color value input and its own igc-select
switcher (hex/RGB/HSL). Changing format re-serializes the existing color without altering it. The
hide-formats attribute removes the format switcher; the format switcher's own rendering is cached
via lit/directives/cache to avoid re-creating the igc-select on unrelated re-renders.
The eye-dropper button is only rendered enabled when 'EyeDropper' in globalThis (support is
captured once, at construction time). Selecting a color through it sets value, re-syncs the canvas
and emits igcInput. The copy button writes the current value to the clipboard via
navigator.clipboard.writeText, silently ignoring failures or an unavailable API.
When swatches is non-empty, a swatch grid is rendered below a divider; clicking a swatch reads the
color from the button's aria-label, assigns it to value, re-syncs the canvas and emits
igcInput.
The dropdown closes on outside click (via the root-click controller), on Escape and on
Alt+ArrowUp (returning focus to the anchor), and opens on Alt+ArrowDown; these keybindings are
skipped while disabled. As a form-associated, required-capable element (via
FormAssociatedRequiredMixin), the component supports value/state restoration:
_restoreDefaultValue() re-parses the default form value, updates the color and re-syncs the
canvas. Validation failures are surfaced through the value-missing, custom-error and invalid
slots, rendered via a shared igc-validator helper-text container.
The remaining static labels are accessibility-only text, not visible copy: the hue/alpha slider
aria-labels (Hue, Alpha slider), a handful of visually-hidden labels (Alpha value,
Color format, Color value input), the format switcher's item text (Hex, RGB, HSL), and the
visually-hidden button text (Open color picker, Copy color value to clipboard, Pick a color from the screen). Color values themselves are locale-independent CSS strings.
| Keys | Description |
|---|---|
Alt+ArrowDown |
Opens the picker dropdown. |
Escape |
Closes the picker dropdown (when open) and returns focus to the anchor. |
Alt+ArrowUp |
Closes the picker dropdown (when open) and returns focus to the anchor. |
Arrow* |
When the gradient canvas marker is focused, moves the marker by one step in the pressed direction, updating saturation/value. |
Tab |
Moves focus between the interactive elements inside the open picker (focus is trapped while open). |
The above keybindings are skipped while the component is
disabled.
| Property | Attribute | Reflect | Type | Default | Description |
|---|---|---|---|---|---|
value |
value |
No | string |
'' |
The current color, serialized in the active format. Accepts any valid CSS color string; empty/invalid clears. |
label |
label |
No | string | undefined |
— | The label of the anchor. In mode="input" it is forwarded to the anchor input's own label. |
format |
format |
No | 'hex' | 'rgb' | 'hsl' |
'hex' |
The output format used when serializing the value string. |
mode |
mode |
No | 'default' | 'input' |
'default' |
Whether the anchor is a trigger button (default) or an editable text field with a swatch prefix (input). |
swatches |
— | No | string[] |
[] |
Predefined color strings rendered as a selectable swatch grid. Not reflected as an attribute. |
hideFormats |
hide-formats |
Yes | boolean |
false |
Whether to hide the format switcher. |
showAlpha |
show-alpha |
Yes | boolean |
false |
Whether to show the alpha slider and its numeric input. |
required |
required |
Yes | boolean |
false |
Whether the control is required for form validation. (inherited, form-associated) |
open |
open |
Yes | boolean |
false |
The open state of the picker dropdown. (inherited) |
name |
name |
Yes | string |
— | The name of the control for form submission. (inherited, form-associated) |
disabled |
disabled |
Yes | boolean |
false |
The disabled state of the control; also skips the open/close keybindings. (inherited, form-associated) |
invalid |
invalid |
No | boolean |
false |
Sets/returns the invalid (visual) state of the control. (inherited, form-associated) |
| Method | Return type | Description |
|---|---|---|
show |
Promise<boolean> |
Opens the picker dropdown. (inherited) |
hide |
Promise<boolean> |
Closes the picker dropdown. (inherited) |
toggle |
Promise<boolean> |
Toggles the open state of the picker dropdown. (inherited) |
checkValidity |
boolean |
Checks the validity of the control without showing a message. (inherited) |
reportValidity |
boolean |
Checks the validity of the control and reports it to the user. (inherited) |
setCustomValidity |
void |
Sets a custom validation message. Pass an empty string to clear. (inherited) |
| Event | Cancelable | Type | Description |
|---|---|---|---|
igcInput |
No | CustomEvent<string> |
Emitted on every interim change (dragging the canvas/sliders, typing, swatches, eye-dropper). |
igcChange |
No | CustomEvent<string> |
Emitted once, when the committed value has changed and focus leaves the component entirely. |
igcOpening |
Yes | CustomEvent<void> |
Emitted just before the picker dropdown is opened. |
igcOpened |
No | CustomEvent<void> |
Emitted after the picker dropdown is opened. |
igcClosing |
Yes | CustomEvent<void> |
Emitted just before the picker dropdown is closed. |
igcClosed |
No | CustomEvent<void> |
Emitted after the picker dropdown is closed. |
| Slot | Description |
|---|---|
value-missing |
Renders content when the required validation fails. |
custom-error |
Renders content when setCustomValidity(message) is set. |
invalid |
Renders content when the component is in an invalid state. |
helper-text |
Renders content below the anchor, alongside any validation messages. |
| Part | Description |
|---|---|
anchor |
The trigger element that opens the picker (the button in default mode, or the swatch prefix in input mode). |
empty |
Applied alongside anchor when no color value is set, rendering a checkered background. |
label |
The label rendered above the anchor in default mode. |
picker |
The popover container holding the canvas, sliders, inputs and swatches. |
main-row |
The row containing the hue slider and the copy/eyedropper buttons. |
alpha-row |
The row containing the alpha slider and input, rendered when show-alpha is set. |
inputs-row |
The row containing the format select and the color value input. |
buttons |
The wrapper around the copy and eyedropper buttons. |
hue |
The hue range slider. |
alpha |
The alpha (opacity) range slider. |
copy |
The copy-to-clipboard icon button. |
eye-dropper |
The eye-dropper icon button (rendered disabled when the EyeDropper API is unavailable). |
format-select |
The select control used to switch the color string format. |
swatches |
The container of predefined color swatches. |
swatch |
An individual predefined color swatch button. |
| Variable | Description |
|---|---|
--current-color |
The current color, applied to the preview thumb and slider thumbs. Updated on every change. |
--hue-slider-track |
The background gradient of the hue slider track. |
--alpha-slider-track |
The layered background image of the alpha slider track (gradient over a checkerboard). |
--alpha-track-position |
The background-position list applied to the alpha slider track layers. |
--alpha-track-size |
The background-size list applied to the alpha slider track layers. |
| State | Description |
|---|---|
:state(ig-invalid) |
Applied to the host while the control is in an invalid state, via the form-associated mixin. |
- Verify that the component renders and is accessible in the closed state (shadow DOM and light DOM a11y audits).
- Verify that the component renders and is accessible in the open state (shadow DOM and light DOM a11y audits).
- Verify default property values:
valueis'',formatis'hex',modeis'default',hideFormatsandshowAlphaarefalse,swatchesis empty,openisfalse. - Verify that
open,hide-formats,show-alphaandrequiredare reflected to the DOM. - Verify that assigning a hex/rgb/rgba/hsl/hsla/named color string to
valueparses correctly and re-serializes in the activeformat. - Verify the empty-value sentinel:
valuedefaults to''; the anchor renders a checkeredpart="anchor empty"background while empty; assigning'',nullorundefined(or an invalid string, via the popover/anchor input) reverts to the empty value. - Verify that changing
formatre-serializes the existing color without changing the represented color, and that the popover's color value input reflects the new format. - Verify that the hue slider, the alpha slider and the alpha numeric input update the corresponding
channel and are reflected in the serialized
value. - Verify that a color pick from the
igc-picker-canvas(igcColorPickedwith{x, y}percentages) updates HSV saturation/value (not HSL saturation) while preserving hue. - Verify that the canvas marker position is synchronized with the current color's HSV saturation/value after open and after value changes.
- Verify that the eye-dropper button is disabled when
EyeDropperis unavailable, and that a successful pick (mockedEyeDropper) updatesvalue. - Verify that the copy button writes the current value to the clipboard.
- Verify that swatches render only when provided, that each renders its color via
aria-label, and that clicking one setsvalueand emitsigcInput. - Verify that
hide-formatsremoves the format select. - Verify open/close behavior:
show/hide/toggle, anchor click, outside click,Escape,Alt+ArrowDown/Alt+ArrowUp(skipped whiledisabled), including theigcOpening/igcOpened/igcClosing/igcClosedevent sequence, cancelation of the cancelable events, and focus returning to the anchor on close. - Verify
igcInput/igcChangesemantics:igcInputfires on every interim interaction;igcChangefires exactly once when the committed value differs from the value at focus-in and focus leaves the component entirely (not on internal focus moves, not when the value is unchanged). - Verify
mode="input"specifics: the anchor renders anigc-inputinstead of a button; clicking the prefix swatch opens the picker; committing a color via the anchor input works;requiredandinvalidare forwarded to the anchor input. - Verify rendering details: the light-DOM label renders only in default mode with
labelset;disabledreflects onto the button anchor. - Verify form association: the control submits its
valueundername, restores its default value on form reset, and respectsdisabled(including ancestor<fieldset disabled>). - Verify validation:
checkValidity,reportValidity,setCustomValidity,required(fails when empty, passes once a value is set) and theinvalidstate behave as expected; the control is marked touched on blur, and invalid styling correctly clears after a form reset. - Verify the
value-missing/custom-error/invalidvalidation message slots render/project correctly for the corresponding validity state. - Verify
igc-picker-canvasin isolation: marker rendering andgetMarkerDimensions(),x/y/currentColorreflecting into the rendered marker, arrow-key nudging with boundary clamping and rounded-percentageigcColorPickedemissions, pointer drag start/move/lost-capture behavior, and thatigcColorPickednever bubbles. - Verify the
ColorModel/parseColor/isValidColorutilities: color-space conversions, the empty sentinel, and deterministic parsing (including hex strings without a leading#, and rejection of invalid strings without leaking previously parsed state).
The anchor is either an igc-button (default mode), labelled via visually-hidden text, or an
igc-input (input mode), exposing label directly. Both anchors set aria-haspopup="dialog" and
reference the validation/helper-text container via aria-describedby="color-picker-helper-text" so
helper and validation messages are announced. The hue and alpha sliders are native
input[type="range"] elements labelled via aria-label (Hue, Alpha slider). The gradient canvas
marker is focusable and operable with the arrow keys. The format switcher is an igc-select with a
visually-hidden label; the color value input carries a visually-hidden label. The copy and
eye-dropper buttons expose visually-hidden text, and each swatch button exposes its color via
aria-label. While the picker is open, focus is trapped within it through igc-focus-trap, and the
inert picker content is excluded from the tab order while closed.
- EyeDropper API
- Clipboard.writeText()
input[type="range"]- Form-associated custom elements (ElementInternals)
- CSS Custom State Set
The component should work in a Right-To-Left context without additional setup or configuration. The sliders and picker layout follow the document direction; color values are direction-independent.
- The eye-dropper button is always rendered but only enabled when the platform implements the
EyeDropperAPI; support is captured once at construction time, so injecting/removing the global after the component is created has no effect. - Copying to the clipboard depends on the
navigator.clipboardAPI and a secure context; it silently no-ops where unavailable or denied. -
valueaccepts any valid CSS color string, but the externally observable value is always normalized to the activeformat; round-tripping an input string is not guaranteed to be byte-for-byte identical. An empty, whitespace-only or invalid string clears the value instead of reverting, except when committed through the popover/anchor color input, where a non-empty invalid string reverts to the last valid color instead. - Named colors and other non-hex/rgb/hsl strings are resolved through an
OffscreenCanvas2D context; environments without canvas support (or an invalid/empty string) resolve to the empty value rather than a default color. - Static UI labels are accessibility-only (
aria-label/visually-hidden text) and are not currently localized.