Description
Lets users select a colour through a visual hue/saturation canvas, a hue slider, an opacity slider, and a hex code input field. The selected colour value is returned as a hex string.
Appears in admin configuration panels — for example, when setting a project brand colour, customising a map layer colour, or configuring a chart series colour in a consultation report.
Some configuration workflows require a freeform colour value that cannot be constrained to a predefined palette. The colour picker gives users a controlled, accessible mechanism for specifying arbitrary colours without requiring them to know hex codes.
Anatomy
| Part | Required? | Notes |
|---|---|---|
| Trigger swatch | Required | A small filled square showing the currently selected colour. Clicking it opens the picker popover. Size is 24x24px with --op-radius-sm border-radius. |
| Hex input field | Required | A text input pre-populated with the current hex value (e.g. #3D9970). Allows direct keyboard entry of a hex code. Validates on blur. |
| Colour canvas | Required | A 2D saturation/lightness gradient canvas derived from the current hue. Users drag a circular thumb to set saturation and lightness. |
| Hue slider | Required | A horizontal rainbow gradient slider for selecting the base hue (0–360°). Rendered as a range input with custom styling. |
| Opacity slider | Optional | A horizontal checkerboard-backed slider for setting alpha (0–100%). Only shown when the consuming context requires alpha support. |
| Preset swatches row | Optional | A row of up to 8 clickable colour swatches representing recently used or context-recommended colours. |
| Popover container | Required | The floating panel that wraps all interactive picker elements. Positioned below the trigger swatch by default, flipping above when viewport space is limited. |
| Label | Recommended | A visible text label associated with the trigger via aria-labelledby. Describes what the colour is being applied to (e.g. 'Project brand colour'). |
Variants
Hex only
A compact inline input showing only the hex field and trigger swatch, with no popover canvas.
When the user is expected to know the exact colour value (e.g. entering a brand hex code from a style guide).
Full picker
The complete popover with canvas, hue slider, hex input, and optional opacity slider.
When users need to explore and select a colour freely, such as customising a map overlay or chart series.
Full picker with presets
Full picker with an additional row of preset swatches drawn from recently used or context-recommended colours.
When the configuration panel has an established set of common choices (e.g. standard map layer colours for a council).
States
| State | Behaviour |
|---|---|
| Default | Trigger swatch displays the current colour. Hex input shows the corresponding hex value. Popover is closed. |
| Open | Popover is visible. Focus moves to the colour canvas on open. Hex input and sliders are interactive. |
| Focused (trigger) | Trigger swatch shows a 3px focus ring using --op-color-interactive-focus. Press Enter or Space to open the popover. |
| Focused (hex input) | Hex input shows standard focus ring. User can type a valid hex code. Canvas and sliders update live on valid input. |
| Invalid | If the hex input value is not a valid colour on blur, the input border switches to --op-color-status-error and an inline error message appears below the field. |
| Disabled | Trigger swatch and hex input are non-interactive. Swatch has 40% opacity. Popover cannot be opened. aria-disabled='true' on the trigger. |
| Read-only | The selected colour is displayed but cannot be changed. Trigger swatch is not focusable. Hex input has readonly attribute. |
Usage guidelines
When to use
- When a configuration field requires a freeform colour value that cannot be limited to a fixed palette — for example, a project brand colour in Open Point's project settings.
- When setting the display colour of a map layer, chart series, or category legend item in a consultation report.
- When an admin needs to match an exact organisational brand colour by entering a known hex code.
When not to use
- When the valid colour options are limited to the Orbit token palette — use a token swatch selector or a select component instead to prevent off-brand choices.
- When colour is used purely as a status indicator — use status tokens (--op-color-status-*) and the Badge or Tag component instead.
- In public-facing Social Point forms — colour choice is an admin configuration concern, not a community input. Avoid exposing this component to end users.
- When the colour must meet a specific contrast ratio against a known background — the colour picker does not validate WCAG contrast. Validate programmatically and surface a warning if the chosen colour fails.
Do / Don't
Do
Always pair the colour picker with a descriptive label (e.g. 'Marker colour') so users understand what the colour applies to.
Don't
Don't use the trigger swatch alone without a label — icon-only or swatch-only controls are not self-explanatory for all users.
Do
Show a preview of the colour applied in context (e.g. a sample map pin or chart bar) near the picker when possible.
Don't
Don't rely on the swatch alone to communicate colour impact — users may not be able to visualise how the colour will look in the actual UI.
Do
Validate the hex input on blur and surface a clear error message if the value is invalid (e.g. 'Enter a valid hex code, such as #3D9970').
Don't
Don't silently reset to the previous value on invalid input without notifying the user.
Do
Persist the selected value immediately on close so the swatch reflects the user's last confirmed choice.
Don't
Don't require a separate 'Apply' button inside the popover — this adds unnecessary steps for an already-deliberate interaction.
Layout & Spacing
Trigger swatch: 24x24px, --op-radius-sm, border 1px solid --op-color-border-default. Trigger swatch + hex input gap: --op-space-8 (8px). Popover container padding: --op-space-16 (16px). Colour canvas: 240px wide x 160px tall. Margin-bottom --op-space-12 (12px). Hue slider: full width of canvas. Margin-bottom --op-space-8 (8px). Opacity slider (when shown): full width of canvas. Margin-bottom --op-space-12 (12px). Hex input within popover: full width. Margin-bottom --op-space-12 (12px) when presets follow. Preset swatches: 20x20px each, --op-radius-sm, gap --op-space-4 (4px). Max 8 per row. Popover min-width: 272px.
Tokens
| Part | Token | Value |
|---|---|---|
| Trigger swatch border | --op-color-border-default | 1px solid border. Switches to --op-color-interactive-default on focus. |
| Focus ring (trigger and inputs) | --op-color-interactive-focus | 3px solid outline, 2px offset. |
| Hex input border (default) | --op-color-border-default | 1px solid. |
| Hex input border (invalid) | --op-color-status-error | 1px solid. Paired with error message text in --op-color-status-error. |
| Popover background | --op-color-bg-primary | White surface. Box shadow: --op-shadow-med. |
| Canvas thumb | --op-color-bg-primary | White circle, 14px diameter, border 2px solid --op-color-bg-primary, drop-shadow for contrast on dark colours. |
| Slider track (hue) | N/A — rendered as a CSS hue gradient | No token; the hue gradient is computed from 0–360 hsl values. |
| Slider thumb | --op-color-bg-primary | White circle, 16px diameter, border 2px solid --op-color-border-default. |
| Preset swatch border | --op-color-border-default | 1px solid. Selected swatch uses 2px --op-color-interactive-default ring. |
| Error message text | --op-color-status-error | --op-text-sm, paired with an error icon. |
Engineering notes
- The colour picker is a custom component — there is no wa-* web component equivalent. It is implemented as a Web Component or React component wrapping a native fallback for environments that do not support the custom build.
- The canonical output value is a lowercase 6-digit hex string (e.g. #3d9970). Strip the leading # before storing if the data layer expects a raw hex value.
- When alpha/opacity support is enabled, output RGBA as a CSS rgba() string or an 8-digit hex. Document which format the consuming API expects.
- The hex input must accept values with or without a leading # and normalise on blur. Reject values shorter than 3 or 6 meaningful characters.
- Popover positioning should use floating-ui or an equivalent library to handle viewport edge cases. Apply a z-index above overlays (--op-z-popover).
- Colour canvas mouse and touch events should be throttled (requestAnimationFrame) to avoid performance issues during drag on lower-powered devices.
- Emit a 'change' event with the new hex value on every meaningful update (canvas drag end, slider change, hex input blur on valid value). Emit an 'input' event for live preview updates during drag.
- When used in a form, bind the hidden input name/value to the hex string so the colour is submitted with the form data.
Keyboard interaction
| Key | Action |
|---|---|
| Tab | Moves focus to the trigger swatch, then (when popover is open) through the hex input, hue slider, opacity slider (if shown), and preset swatches in DOM order. |
| Enter / Space | Opens the popover when the trigger swatch is focused. Activates a preset swatch when it is focused. |
| Escape | Closes the popover and returns focus to the trigger swatch. |
| Arrow keys (on colour canvas) | Move the canvas thumb in 1% increments (saturation/lightness). Hold Shift for 10% increments. |
| Arrow keys (on sliders) | Adjust hue or opacity in 1-unit increments. Hold Shift for 10-unit increments. |
Why it matters
Government configuration panels are used by a wide range of staff, including those who rely on keyboard navigation or screen readers. A colour picker that is only operable via mouse excludes keyboard-only users entirely. Providing full keyboard control of the canvas and sliders, and supporting direct hex input as a primary interaction path, ensures all users can complete colour configuration tasks.
Focus
When the popover opens, focus moves to the colour canvas element (role="slider" with aria-label describing saturation and lightness). When the popover closes (Escape or click outside), focus returns to the trigger swatch. The popover uses role="dialog" with aria-label="Colour picker" to establish a focus trap while open, preventing focus from escaping to underlying page content.
ARIA
| Role or attribute | When to use | Example |
|---|---|---|
role="slider" | Applied to the colour canvas interactive region and to each range slider (hue, opacity). | <div role="slider" aria-label="Saturation and lightness" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" tabindex="0"> |
aria-label | Applied to the trigger swatch button. Should reference the field label (e.g. 'Project brand colour — open colour picker'). | <button aria-label="Project brand colour — open colour picker" aria-expanded="false"> |
aria-expanded | Applied to the trigger swatch button. Set to 'true' when the popover is open, 'false' when closed. | <button aria-expanded="true"> |
aria-live="polite" | Applied to an off-screen announcement region. Announces the current hex value when selection changes so screen reader users hear live feedback. | <span aria-live="polite" class="sr-only">Selected colour: #3D9970</span> |
aria-invalid | Applied to the hex input when the entered value is not a valid colour. | <input type="text" aria-invalid="true" aria-describedby="hex-error"> |
aria-describedby | Links the hex input to its error message element when in an invalid state. | <input aria-describedby="hex-error"> <span id="hex-error">Enter a valid hex code, such as #3D9970</span> |
role="dialog" | Applied to the popover container to create a focus trap and communicate modal intent to assistive technology. | <div role="dialog" aria-label="Colour picker" aria-modal="true"> |
Contrast
The colour picker UI chrome (labels, hex input text, slider track outlines) must meet 4.5:1 contrast against --op-color-bg-primary. Text in the hex input uses --op-color-text-primary on --op-color-bg-primary, which satisfies this requirement.
The picker does NOT validate whether the user's chosen colour meets contrast requirements for its intended use. If the chosen colour will be used as text or an interactive element colour, the consuming component or form must perform contrast validation and surface a warning. Do not silently accept colours that would produce inaccessible results downstream.
The canvas thumb and slider thumb must remain visible against both light and dark regions of the gradient. Use a white fill with a contrasting drop-shadow to ensure visibility across the full hue/saturation range.
Touch targets
The trigger swatch at 24x24px is below the 44x44px minimum touch target. Wrap it in a button element with padding to bring the interactive hit area to at least 44x44px without enlarging the visual swatch. The canvas thumb (14px) and slider thumbs (16px) should have a touch target overlay of at least 44x44px centred on the visual element.
Things to avoid
- Do not rely solely on colour to communicate the selected value — always show the hex code alongside the swatch so users with colour vision deficiencies can confirm their selection.
- Do not close the popover on any mouse-down outside without also supporting Escape key dismissal.
- Do not disable the hex input as a 'simplified' UI — it is the primary accessible input path for keyboard and screen reader users.
- Do not skip aria-live announcements for canvas drag updates — screen reader users need feedback that the colour is changing as they use arrow keys.