Skip to content

Color picker

Radoslav Karaivanov edited this page Jul 31, 2026 · 1 revision

Color picker specification

Owned By

Team Name: Design and Web Development

Developer name: Radoslav Karaivanov

Designer name:

Requires approval from:

Signed off by:

Revision history

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.

Overview

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.

Acceptance criteria

The igc-color-picker must:

  • be WAI-ARIA compliant.
  • participate in form association (submit its value under its name, support reset, required and 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 value consistently in the currently selected format.
  • 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, rgb and hsl formats without losing the represented color.
  • optionally hide the format switcher via hide-formats and show/hide the alpha slider via show-alpha.
  • render an eye-dropper action only when the platform EyeDropper API 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, Escape and Alt+ArrowUp, and open on Alt+ArrowDown, restoring focus to the anchor.
  • emit igcInput on every interim interaction and igcChange once, 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 via aria-describedby.
  • expose styling hooks for the slider tracks and the current color through CSS custom properties.

User stories

End-user stories

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 input mode).
  • 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+ArrowUp or by clicking outside of it.
  • see a validation message when the field is required and left empty.

Developer stories

As a developer I expect to be able to:

  • bind the picker to a form by setting name and read/write its value as a string.
  • choose the anchor presentation via mode ("default" or "input").
  • choose the serialization format (hex, rgb or hsl) of the emitted value.
  • provide a list of predefined swatches.
  • hide the format switcher with hide-formats and show the alpha slider with show-alpha.
  • mark the control as required, disable it, and surface validation state through the standard form-associated API (checkValidity, reportValidity, setCustomValidity, invalid) and the value-missing/custom-error/invalid/helper-text slots.
  • programmatically open, close or toggle the picker.
  • react to interim and committed value changes through igcInput and igcChange, and to igcOpening, igcOpened, igcClosing and igcClosed.
  • customize the hue/alpha slider tracks and the current color through CSS custom properties.

Functionality

End-user experience

Design Handoff

Developer experience

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:

  1. Drag within the igc-picker-canvas gradient to set HSV saturation and value.
  2. Drag the hue range input to set hue.
  3. Drag the alpha range input (rendered when show-alpha is set) to set opacity, or type an exact alpha value into the accompanying numeric input.
  4. 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.

Localization

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.

Keyboard navigation

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.

API

Properties

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)

Methods

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)

Events

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.

Slots

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.

CSS Shadow Parts

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.

CSS Variables

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.

CSS Custom States

State Description
:state(ig-invalid) Applied to the host while the control is in an invalid state, via the form-associated mixin.

Test scenarios

Automation

  • 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: value is '', format is 'hex', mode is 'default', hideFormats and showAlpha are false, swatches is empty, open is false.
  • Verify that open, hide-formats, show-alpha and required are reflected to the DOM.
  • Verify that assigning a hex/rgb/rgba/hsl/hsla/named color string to value parses correctly and re-serializes in the active format.
  • Verify the empty-value sentinel: value defaults to ''; the anchor renders a checkered part="anchor empty" background while empty; assigning '', null or undefined (or an invalid string, via the popover/anchor input) reverts to the empty value.
  • Verify that changing format re-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 (igcColorPicked with {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 EyeDropper is unavailable, and that a successful pick (mocked EyeDropper) updates value.
  • 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 sets value and emits igcInput.
  • Verify that hide-formats removes the format select.
  • Verify open/close behavior: show/hide/toggle, anchor click, outside click, Escape, Alt+ArrowDown/Alt+ArrowUp (skipped while disabled), including the igcOpening/igcOpened/igcClosing/igcClosed event sequence, cancelation of the cancelable events, and focus returning to the anchor on close.
  • Verify igcInput/igcChange semantics: igcInput fires on every interim interaction; igcChange fires 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 an igc-input instead of a button; clicking the prefix swatch opens the picker; committing a color via the anchor input works; required and invalid are forwarded to the anchor input.
  • Verify rendering details: the light-DOM label renders only in default mode with label set; disabled reflects onto the button anchor.
  • Verify form association: the control submits its value under name, restores its default value on form reset, and respects disabled (including ancestor <fieldset disabled>).
  • Verify validation: checkValidity, reportValidity, setCustomValidity, required (fails when empty, passes once a value is set) and the invalid state 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/invalid validation message slots render/project correctly for the corresponding validity state.
  • Verify igc-picker-canvas in isolation: marker rendering and getMarkerDimensions(), x/y/ currentColor reflecting into the rendered marker, arrow-key nudging with boundary clamping and rounded-percentage igcColorPicked emissions, pointer drag start/move/lost-capture behavior, and that igcColorPicked never bubbles.
  • Verify the ColorModel/parseColor/isValidColor utilities: 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).

Accessibility

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.

References

RTL

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.

Assumptions and limitations

  • The eye-dropper button is always rendered but only enabled when the platform implements the EyeDropper API; 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.clipboard API and a secure context; it silently no-ops where unavailable or denied.
  • value accepts any valid CSS color string, but the externally observable value is always normalized to the active format; 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 OffscreenCanvas 2D 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.

Clone this wiki locally