Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Color Picker

A color editor: a two-dimensional Area with its AreaThumb, ChannelSliders for the hue, the alpha or any other channel, a hue Wheel, a text Input, a number ChannelInput per channel, a Swatch and a Value that show the color, and an EyeDropper that picks one from the screen.

Rendered by React 19.3.1

Loading React…
import { ColorPicker } from '@hamur/react/color-picker'
import { ColorSwatchPicker } from '@hamur/react/color-swatch-picker'
import { Field } from '@hamur/react/field'

const presets = ['#e5484d', '#f76b15', '#ffc53d', '#46a758', '#0090ff', '#8e4ec6']

export default function ColorPickerExample() {
  return (
    <Field.Root className="field">
      <Field.Label>Accent color</Field.Label>
      <ColorPicker.Root className="color-picker-row" defaultValue="#6699cc">
        <ColorPicker.Trigger className="button color-picker-trigger">
          <ColorPicker.Swatch className="color-swatch" />
        </ColorPicker.Trigger>
        <ColorPicker.Value className="color-picker-value" />
        <ColorPicker.Portal>
          <ColorPicker.Positioner sideOffset={8}>
            <ColorPicker.Popup className="popover color-picker-popup">
              <ColorPicker.Area className="color-picker-area">
                <ColorPicker.AreaThumb className="color-picker-thumb" />
              </ColorPicker.Area>
              <ColorPicker.ChannelSlider
                channel="hue"
                className="color-picker-slider"
              >
                <ColorPicker.ChannelSliderThumb
                  className="color-picker-thumb"
                />
              </ColorPicker.ChannelSlider>
              <ColorPicker.ChannelSlider
                channel="alpha"
                className="color-picker-slider color-picker-alpha"
              >
                <ColorPicker.ChannelSliderThumb
                  className="color-picker-thumb"
                />
              </ColorPicker.ChannelSlider>
              <ColorPicker.Input className="input color-picker-input" />
              <ColorSwatchPicker.Root className="color-swatch-picker" aria-label="Presets">
                {presets.map((color) => (
                  <ColorSwatchPicker.Item
                    key={color}
                    value={color}
                    className="color-swatch-picker-item"
                  />
                ))}
              </ColorSwatchPicker.Root>
              <ColorPicker.Close className="button">
                Done
              </ColorPicker.Close>
            </ColorPicker.Popup>
          </ColorPicker.Positioner>
        </ColorPicker.Portal>
      </ColorPicker.Root>
    </Field.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<ColorPicker.Root>
  <ColorPicker.Trigger>
    <ColorPicker.Swatch />
  </ColorPicker.Trigger>
  <ColorPicker.Value />
  <ColorPicker.Portal>
    <ColorPicker.Positioner>
      <ColorPicker.Popup>
        <ColorPicker.Area>
          <ColorPicker.AreaThumb />
        </ColorPicker.Area>
        <ColorPicker.ChannelSlider>
          <ColorPicker.ChannelSliderThumb />
        </ColorPicker.ChannelSlider>
        <ColorPicker.Input />
        <ColorPicker.Close />
      </ColorPicker.Popup>
    </ColorPicker.Positioner>
  </ColorPicker.Portal>
</ColorPicker.Root>

// Not in this example: ColorPicker.ChannelInput, ColorPicker.EyeDropper, ColorPicker.Wheel, ColorPicker.WheelThumb

Examples

Inline

No Trigger: the Area, the hue slider, a Swatch, the Input and an EyeDropper in place, with the last committed color under them from onValueCommitted.

ColorPickerInline.tsx
import * as React from 'react'
import { ColorPicker } from '@hamur/react/color-picker'

export default function ColorPickerInlineExample() {
  const [committed, setCommitted] = React.useState<string | null>('#6699cc')
  return (
    <div className="stack">
      <ColorPicker.Root
        className="color-picker"
        aria-label="Background"
        defaultValue="#6699cc"
        onValueCommitted={(value) => setCommitted(value)}
      >
        <ColorPicker.Area className="color-picker-area">
          <ColorPicker.AreaThumb className="color-picker-thumb" />
        </ColorPicker.Area>
        <ColorPicker.ChannelSlider channel="hue" className="color-picker-slider">
          <ColorPicker.ChannelSliderThumb className="color-picker-thumb" />
        </ColorPicker.ChannelSlider>
        <div className="color-picker-row">
          <ColorPicker.Swatch className="color-swatch" />
          <ColorPicker.Input className="input color-picker-input" />
          <ColorPicker.EyeDropper className="button color-picker-eye-dropper">
            <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
              <path d="M10.5 2.5l3 3M9 4l3 3-6.5 6.5H3v-3z" />
            </svg>
          </ColorPicker.EyeDropper>
        </div>
      </ColorPicker.Root>
      <p className="muted">Committed: {committed ?? 'none'}</p>
    </div>
  )
}

Oklch

colorSpace="oklch" with gamut="display-p3" and format="oklch": chroma by lightness, the thumb dashed outside the gamut, and the value as oklch() and color(display-p3 …).

ColorPickerOklch.tsx
import { ColorPicker } from '@hamur/react/color-picker'

export default function ColorPickerOklchExample() {
  return (
    <ColorPicker.Root
      className="color-picker"
      aria-label="Brand green"
      colorSpace="oklch"
      gamut="display-p3"
      format="oklch"
      defaultValue="oklch(70% 0.15 150)"
    >
      <ColorPicker.Area className="color-picker-area">
        <ColorPicker.AreaThumb className="color-picker-thumb" />
      </ColorPicker.Area>
      <ColorPicker.ChannelSlider channel="hue" className="color-picker-slider">
        <ColorPicker.ChannelSliderThumb className="color-picker-thumb" />
      </ColorPicker.ChannelSlider>
      <dl className="color-picker-values">
        <dt>OKLCH</dt>
        <dd>
          <ColorPicker.Value className="color-picker-value" />
        </dd>
        <dt>Display P3</dt>
        <dd>
          <ColorPicker.Value className="color-picker-value" format="display-p3" />
        </dd>
      </dl>
    </ColorPicker.Root>
  )
}

Wheel

A hue Wheel with its thumb on the ring, 0° at the top and clockwise, and a Swatch and Value under it.

ColorPickerWheel.tsx
import { ColorPicker } from '@hamur/react/color-picker'

export default function ColorPickerWheelExample() {
  return (
    <ColorPicker.Root className="color-picker" aria-label="Highlight" defaultValue="#6699cc">
      <ColorPicker.Wheel className="color-picker-wheel">
        <ColorPicker.WheelThumb className="color-picker-thumb color-picker-wheel-thumb" />
      </ColorPicker.Wheel>
      <div className="color-picker-row">
        <ColorPicker.Swatch className="color-swatch" />
        <ColorPicker.Value className="color-picker-value" />
      </div>
    </ColorPicker.Root>
  )
}

Channels

Red, green and blue ChannelInputs clamped to 0–255, the Input, and a Toggle Group that sets format to hex, RGB or HSL.

ColorPickerChannels.tsx
import * as React from 'react'
import { ColorPicker } from '@hamur/react/color-picker'
import { Toggle } from '@hamur/react/toggle'
import { ToggleGroup } from '@hamur/react/toggle-group'

type Format = 'hex' | 'rgb' | 'hsl'

const channels = [
  { channel: 'red', short: 'R' },
  { channel: 'green', short: 'G' },
  { channel: 'blue', short: 'B' },
] as const

export default function ColorPickerChannelsExample() {
  const [format, setFormat] = React.useState<Format>('hex')
  return (
    <ColorPicker.Root
      className="color-picker"
      aria-label="Text color"
      defaultValue="#6699cc"
      format={format}
    >
      <ToggleGroup
        className="toggle-group"
        aria-label="Format"
        value={[format]}
        onValueChange={(next) => setFormat((next[0] as Format | undefined) ?? format)}
      >
        <Toggle className="toggle" value="hex">
          Hex
        </Toggle>
        <Toggle className="toggle" value="rgb">
          RGB
        </Toggle>
        <Toggle className="toggle" value="hsl">
          HSL
        </Toggle>
      </ToggleGroup>
      <div className="color-picker-channels">
        {channels.map(({ channel, short }) => (
          <div key={channel} className="color-picker-channel">
            <span aria-hidden="true">{short}</span>
            <ColorPicker.ChannelInput channel={channel} className="input" />
          </div>
        ))}
      </div>
      <div className="color-picker-row">
        <ColorPicker.Swatch className="color-swatch" />
        <ColorPicker.Input className="input color-picker-input" />
      </div>
    </ColorPicker.Root>
  )
}

Form

A required picker in a Field of a Form with an Input and presets: Send without a color shows "Choose a brand color before you send.", a preset submits brand=.

ColorPickerForm.tsx
import * as React from 'react'
import { ColorPicker } from '@hamur/react/color-picker'
import { ColorSwatchPicker } from '@hamur/react/color-swatch-picker'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'

const presets = ['#e5484d', '#f76b15', '#ffc53d', '#46a758', '#0090ff', '#8e4ec6']

export default function ColorPickerFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="brand" className="field">
        <Field.Label>Brand color</Field.Label>
        <ColorPicker.Root className="color-picker" required>
          <ColorPicker.Input className="input color-picker-input" placeholder="#rrggbb" />
          <ColorSwatchPicker.Root className="color-swatch-picker" aria-label="Presets">
            {presets.map((color) => (
              <ColorSwatchPicker.Item
                key={color}
                value={color}
                className="color-swatch-picker-item"
              />
            ))}
          </ColorSwatchPicker.Root>
        </ColorPicker.Root>
        <Field.Description className="muted">Type a color or pick a preset.</Field.Description>
        <Field.Error className="error" match="valueMissing">
          Choose a brand color before you send.
        </Field.Error>
      </Field.Root>
      <button className="button" type="submit">
        Send
      </button>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

API reference

70 props across 17 parts, in React syntax. className, style, render are listed once, under Shared props.

AreaColorPicker.Area

The two-dimensional surface of two channels (saturation by brightness by default). A press sets both channels under the pointer and a drag continues outside it. Renders a <div>.

Area props
PropTypeDefault
xChannelThe channel along the x axis.ColorChannel—
yChannelThe channel along the y axis.ColorChannel—
xStepThe x channel's step for the arrow keys and the pointer.number—
yStepThe y channel's step for the arrow keys and the pointer.number—
xLargeStepThe x channel's step with Shift.number—
yLargeStepThe y channel's step with Shift and Page Up or Page Down.number—
Area data attributes
Data attributeDescription
data-draggingPresent while a pointer drags on the area.
data-disabledPresent when the color picker is disabled.
data-readonlyPresent when the color picker is read-only.
Area CSS variables
CSS variableDescription
--area-backgroundThe colors of the area at the current value of the third channel, as a CSS background.
--hue-colorThe current hue at full saturation.

AreaThumbColorPicker.AreaThumb

The thumb of the area, placed from the two channels without measuring. It holds two visually hidden range inputs, one per axis, which are one Tab stop: the arrow keys move focus to the input of the axis they change. Renders a <div>.

AreaThumb props
PropTypeDefault
getAriaValueTextReplaces the value text of each input: channel is the input's channel, color the working((channel: ColorChannel, color: WorkingColor) => string)—
AreaThumb data attributes
Data attributeDescription
data-draggingPresent while a pointer drags on the area.
data-focusedPresent while one of the thumb's inputs has focus.
data-out-of-gamutPresent while the position lies outside the gamut (only in oklch); the color shown is mapped into it.

ChannelInputColorPicker.ChannelInput

A number input for one channel, with NumberField's keys and parsing, clamped to the channel's range when it is committed. It submits nothing of its own. Renders an <input>.

ChannelInput props
PropTypeDefault
channelrequiredThe channel the input shows and changes.ColorChannel—
stepThe step of the arrow keys.number—
largeStepThe step with Shift and Page Up or Page Down.number—
smallStepThe step with Alt (or Meta).number—
formatThe Intl.NumberFormat options of the number.Intl.NumberFormatOptions—
aria-roledescriptionA user-friendly description of the input's role for assistive tech.string'Number field'

ChannelSliderColorPicker.ChannelSlider

A slider for one channel of the color (hue, alpha, a channel of the color space, or red, green or blue), with Slider's keys and pointer session. Renders a <div>.

ChannelSlider props
PropTypeDefault
channelrequiredThe channel the slider changes.ColorChannel—
orientationThe slider's orientation.Orientation'horizontal'
stepThe step of the arrow keys and the pointer.number—
largeStepThe step with Shift and Page Up or Page Down.number—
ChannelSlider data attributes
Data attributeDescription
data-orientationThe slider's orientation.
data-draggingPresent while a pointer drags on the slider.
data-disabledPresent when the color picker is disabled.
ChannelSlider CSS variables
CSS variableDescription
--channel-gradientThe channel from its minimum to its maximum at the current color, as a linear-gradient.

ChannelSliderThumbColorPicker.ChannelSliderThumb

The thumb of a channel slider, placed from the channel's value. It holds the visually hidden range input that takes focus and the keys. Renders a <div>.

ChannelSliderThumb props
PropTypeDefault
getAriaValueTextReplaces the value text: channel is the slider's channel, color the working color.((channel: ColorChannel, color: WorkingColor) => string)—
ChannelSliderThumb data attributes
Data attributeDescription
data-draggingPresent while a pointer drags on the slider.
data-focusedPresent while the thumb's input has focus.

CloseColorPicker.Close

A button that closes the popover. Renders a <button>.

Close props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

EyeDropperColorPicker.EyeDropper

A button that picks a color from the screen with the browser's EyeDropper API, keeping the current alpha. Where the browser lacks the API it stays focusable with aria-disabled. Renders a <button>.

EyeDropper props
PropTypeDefault
focusableWhenDisabledWhether the button stays focusable when disabled or unsupported.booleantrue
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
EyeDropper data attributes
Data attributeDescription
data-unsupportedPresent when the browser has no EyeDropper API, and before hydration.
data-activePresent while the eyedropper is open.
data-disabledPresent when the color picker is disabled.

InputColorPicker.Input

A text input for the whole color in a format. The text is committed on Enter, on blur and on a paste; empty text clears the color, and text that does not parse is restored on blur. Escape restores edited text. Renders an <input>.

Input props
PropTypeDefault
formatThe format the input shows.ColorFormat—
Input data attributes
Data attributeDescription
data-invalidPresent while the text does not parse as a color.
data-focusedPresent while the input has focus.

PopupColorPicker.Popup

The dialog that holds the editor parts, named by the Root's label. Focus moves to the Area's x input when it opens, else to ColorPicker.Input. Renders a <div>.

Popup props
PropTypeDefault
initialFocusDetermines the element to focus when the popover is opened.boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)—
finalFocusDetermines the element to focus when the popover is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—

PortalColorPicker.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—

PositionerColorPicker.Positioner

Positions the popover against the trigger. Renders a <div>.

Positioner props
PropTypeDefault
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
alignHow to align the popup relative to the specified side.Align'center'
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
collisionBoundaryAn element or a rectangle that delimits the area that the popup is confined to.Boundary'clipping-ancestors'
collisionPaddingAdditional space to maintain from the edge of the collision boundary.Padding5
stickyWhether to maintain the popup in the viewport afterbooleanfalse
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—
Positioner data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-anchor-hiddenPresent when the anchor is hidden.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
Positioner CSS variables
CSS variableDescription
--available-widthThe available width between the trigger and the edge of the viewport.
--available-heightThe available height between the trigger and the edge of the viewport.
--anchor-widthThe anchor's width.
--anchor-heightThe anchor's height.
--transform-originThe coordinates that this element is anchored to. Used for animations and transitions.
--positioner-widthThe width of the popover's positioner. It is important to set width to this value when using CSS to animate size changes.
--positioner-heightThe height of the popover's positioner. It is important to set height to this value when using CSS to animate size changes.

RootColorPicker.Root

Groups all parts of the color picker and holds the color. With a Trigger the editor parts live in a popup; without one they render inline. and a hidden <input> beside it. Renders a <div>.

Root props
PropTypeDefault
valueThe color, any CSS color string, or null for no color.string | null—
defaultValueThe color when the component is initially rendered.string | nullnull
onValueChangeEvent handler called when the color changes, with the color in format.((value: string | null, eventDetails: ColorPickerRootChangeEventDetails) => void)—
onValueCommittedEvent handler called when an interaction ends: a pointer release, a key press, an Input or((value: string | null, eventDetails: ColorPickerRootCommitEventDetails) => void)—
formatThe format of the emitted and submitted string.ColorFormat'hex'
colorSpaceThe space the Area, the sliders and the Wheel work in.WorkingColorSpace'hsv'
gamutWith colorSpace="oklch", the gamut the controls map the color into.ColorGamut'srgb'
openWhether the popup is open.boolean—
defaultOpenWhether the popup is initially open.booleanfalse
onOpenChangeEvent handler called when the popup opens or closes.((open: boolean, eventDetails: ColorPickerRootOpenChangeEventDetails) => void)—
onOpenChangeCompleteEvent handler called after any animations complete when the popup opens or closes.((open: boolean) => void)—
actionsRefA ref to imperative actions: unmount and close.React.RefObject<PopoverRootActions | null>—
modalWhether the rest of the page is inert while the popup is open.boolean | "trap-focus"false
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the color.booleanfalse
requiredWhether the user must choose a color before submitting a form.booleanfalse
labelsThe texts, in English by default: the channel names, the value text templates, the colorPartial<ColorPickerLabels>—
getColorNameNames a color (in value texts, swatches and the Trigger) in place of the generated name.((color: Color) => string)—
localeThe locale of the numbers in value texts and channel inputs.Intl.LocalesArgument—
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden input.string—
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
Root data attributes
Data attributeDescription
data-disabledPresent when the color picker is disabled.
data-readonlyPresent when the color picker is read-only.
data-requiredPresent when the color picker is required.
data-emptyPresent when there is no color.
data-validPresent when the color picker is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the color picker is in an invalid state (when wrapped in Field.Root).
data-dirtyPresent when the color picker's value has changed (when wrapped in Field.Root).
data-touchedPresent when the color picker has been touched (when wrapped in Field.Root).
data-filledPresent when the color picker has a color (when wrapped in Field.Root).
data-focusedPresent when a part of the color picker has focus (when wrapped in Field.Root).
Root CSS variables
CSS variableDescription
--colorThe current color.
--color-opaqueThe current color at alpha 1.

SwatchColorPicker.Swatch

An image of one color, named by the color's description. Inside ColorPicker.Root it shows the picker's color unless color is given. Renders a <div>.

Swatch props
PropTypeDefault
colorThe color, any CSS color string, or null for no color.string | null—
labelsThe color name words and the name of no color, in English by default.Partial<ColorSwatchLabels>—
getColorNameNames the color in place of the generated name.((color: Color) => string)—
localeThe locale of the transparency percentage.Intl.LocalesArgument—
Swatch data attributes
Data attributeDescription
data-emptyPresent when there is no color.
data-transparentPresent when the color's alpha is below 1.
Swatch CSS variables
CSS variableDescription
--colorThe color.
--color-opaqueThe color at alpha 1.

TriggerColorPicker.Trigger

A button that opens the popup. It is named "Choose color" with the color's name ("Choose color, dark blue") unless it has text or aria-label; a ColorPicker.Swatch inside shows the color. Renders a <button>.

Trigger props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
idID of the trigger.string—
Trigger data attributes
Data attributeDescription
data-popup-openPresent when the popup is open.
data-disabledPresent when the trigger is disabled.
data-emptyPresent when there is no color.

ValueColorPicker.Value

The color as text, in the Root's format or its own. It is plain text, not a live region. Renders a <span>.

Value props
PropTypeDefault
formatThe format of the text.ColorFormat—
childrenThe content, or a function of the value in the format (null for no color) and the workingReact.ReactNode | ((value: string | null, color: WorkingColor) => React.ReactNode)—
Value data attributes
Data attributeDescription
data-emptyPresent when there is no color.

WheelColorPicker.Wheel

A ring that sets the hue by the angle around its center: 0° at the top, clockwise, the same in right-to-left layouts. Renders a <div>.

Wheel props
PropTypeDefault
stepThe hue step of the arrow keys and the pointer, in degrees.number—
largeStepThe hue step with Shift and Page Up or Page Down, in degrees.number—
Wheel data attributes
Data attributeDescription
data-draggingPresent while a pointer drags on the wheel.
Wheel CSS variables
CSS variableDescription
--wheel-gradientThe hues around the wheel, 0° at the top and clockwise, as a conic-gradient.

WheelThumbColorPicker.WheelThumb

The thumb of the wheel, placed on the ring at the hue's angle. It holds the visually hidden range input that takes focus and the keys. Renders a <div>.

WheelThumb data attributes
Data attributeDescription
data-draggingPresent while a pointer drags on the wheel.
data-focusedPresent while the thumb's input has focus.
WheelThumb CSS variables
CSS variableDescription
--wheel-angleThe hue in degrees, the thumb's angle from the top.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
TabMoves to the area's x input, its one Tab stop; the y input is reached with the vertical arrowsAreaThumb
ArrowLeftArrowRightLowers or raises the x channel (mirrored in RTL), with focus on the x inputAreaThumb
ArrowUpArrowDownRaises or lowers the y channel and moves focus to the y inputAreaThumb
HomeEndSets the x channel to its lowest or highestAreaThumb
PageUpPageDownRaises or lowers the y channel by its large stepAreaThumb
ShiftMoves by the channel's large stepAreaThumb
PageUpHomeEndChange the slider's channel; the hue stays 360 at its end, not 0ChannelSliderThumb
ShiftMove the alpha by 1% and by 10%ChannelSliderThumb
PageUpHomeEndTurn the hue; not mirrored in RTLWheelThumb
EnterCommits the typed color and fires onValueCommittedInput
EscapeInputRestores the text and keeps the popup open; a second Escape closes itInput
ArrowUpArrowDownStep the channel; Shift by the large step, Alt by a tenth; the value clamps on commitChannelInput
EnterSpaceOpens the popup and focuses the area's x input, else the first inputTrigger
EscapeCloses the popup and returns focus to the TriggerPopup
EnterSpaceOpens the browser's eyedropper; the picked hex keeps the current alphaEyeDropper

Accessibility

What the parts set
PartSetsSo that
Rootrole="group" named by Field.Label, aria-label or aria-labelledbythe controls are read as one color with its name
AreaThumbtwo visually hidden input type="range", horizontal and vertical, each named by its channel, with its own aria-valuenow and the role description "2D slider"each axis is a real slider with the keys and gestures a screen reader already knows
AreaThumbaria-valuetext that names both channels and the color on focus ("Saturation: 50%, Brightness: 80%, blue"), then only the changed channelthe value is heard as words and numbers, not as colour alone
ChannelSliderThumbone input type="range" named by its channel ("Hue", "Alpha"), with aria-valuetext "210°, blue" or "50%"a slider says its channel and the color it gives
WheelThumbone input type="range" named "Hue"the wheel is the hue slider it looks like
Inputa text input named "Color" (labels.input), autocomplete and spell check off, aria-invalid while the text is not a colora color can be typed or pasted in any CSS syntax
ChannelInputa Number Field input named by its channel, with the role description "Number field"each channel can be typed exactly
Triggera button with aria-haspopup="dialog", named "Choose color" with the color ("Choose color, blue") unless it has text or aria-labelthe button says what it opens and the current color
Popuprole="dialog", named like the Rootthe popup is read with the field it belongs to
Swatchrole="img" named by the color ("blue", "red, 50% transparent", "No color")the color shown is also said
Valuethe color as text, not a live regionthe value can be read without a change being announced on every drag step
EyeDroppera button named "Pick a color from the screen"; aria-disabled and data-unsupported where the browser has no eyedropperthe button stays reachable and says it cannot be used

Focus order

  1. Inline, the Area is one Tab stop (its x input), then each ChannelSlider, the Wheel, the Input, each ChannelInput, a Color Swatch Picker (one Tab stop) and the EyeDropper, in document order. Nothing takes focus on mount.
  2. With a Trigger, opening moves focus to the area's x input, else the Input, else the popup; Escape, Close and a press outside close it and return focus to the Trigger. An outside press commits edited Input text first.
  3. A key on the area's x input that changes y moves focus to the y input, and a horizontal key there moves it back.
  4. A read-only picker keeps its Tab stops; a disabled one has none. A required, empty picker in a Form gets focus on submit: the area's x input, else the Trigger, else the Input.