Accessibility pattern · Form inputs

Color picker

Three ways to set one color, all native: the browser's color input, a hex code field that explains its format, and preset swatches that are radio buttons with names. The contrast against white and black text is worked out with the WCAG formula and written as a ratio and a verdict, not shown as a color.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Brand color

Used for buttons and links on your shop page.

6 characters from 0–9 and A–F. A leading # is fine.

Presets

Text on this color

White text 6.2:1 Passes AA

Black text 3.3:1 Passes AA

Use white text on this color.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to the color input, the hex field, the checked preset (or the first one), and on out of the presets.
Enter or SpaceOn the color input, opens the browser's own picker.
EnterIn the hex field, applies the code, or explains what is wrong with it.
Arrow Right or Arrow DownIn the presets, chooses the next swatch, wrapping at the end.
Arrow Left or Arrow UpIn the presets, chooses the previous swatch.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the presetsPresets, radio group. Indigo, radio button, checked, 1 of 8
Arrow Right chooses the next swatchTeal, radio button, checked, 2 of 8
The color settles for a momentWhite text: 5.4:1, Passes AA. Black text: 3.8:1, Large text only.
A wrong hex code is appliedUse 6 characters from 0–9 and A–F, like 4F46E5.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-color-picker" data-ap-color-picker>
  <div class="ap-color-picker__top">
    <h3 class="ap-color-picker__name">Brand color</h3>
    <p class="ap-color-picker__lead">Used for buttons and links on your shop page.</p>
  </div>

  <div class="ap-color-picker__pick">
    <div class="ap-field">
      <label class="ap-label" for="color-picker-well">Color</label>
      <input class="ap-color-picker__well" id="color-picker-well" type="color" value="#4f46e5" />
    </div>
    <div class="ap-field ap-color-picker__hex">
      <label class="ap-label" for="color-picker-hex">Hex code</label>
      <div class="ap-color-picker__hexbox">
        <span aria-hidden="true">#</span>
        <input class="ap-input" id="color-picker-hex" type="text" value="4F46E5" maxlength="7" autocomplete="off" spellcheck="false" aria-describedby="color-picker-hint color-picker-error" />
      </div>
    </div>
  </div>
  <p class="ap-hint ap-color-picker__hint" id="color-picker-hint">6 characters from 0–9 and A–F. A leading # is fine.</p>
  <p class="ap-error ap-color-picker__error" id="color-picker-error" aria-live="polite"></p>

  <div class="ap-color-picker__presets" role="radiogroup" aria-labelledby="color-picker-presets">
    <div class="ap-color-picker__bar">
      <span class="ap-color-picker__sub" id="color-picker-presets">Presets</span>
      <span class="ap-color-picker__picked" aria-hidden="true" data-ap-picked>Indigo</span>
    </div>
    <div class="ap-color-picker__swatches">
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#4F46E5" checked />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Indigo</span>
      </label>
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#0F766E" />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Teal</span>
      </label>
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#F59E0B" />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Saffron</span>
      </label>
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#BE123C" />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Crimson</span>
      </label>
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#15803D" />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Forest</span>
      </label>
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#0EA5E9" />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Sky</span>
      </label>
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#7E22CE" />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Plum</span>
      </label>
      <label class="ap-color-picker__swatch">
        <input class="ap-color-picker__radio" type="radio" name="color-picker-preset" value="#27272A" />
        <span class="ap-color-picker__chip" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m5 12 5 5L20 7"/></svg></span>
        <span class="ap-color-picker__sr">Charcoal</span>
      </label>
    </div>
  </div>

  <section class="ap-color-picker__contrast" aria-labelledby="color-picker-contrast">
    <h4 class="ap-color-picker__sub" id="color-picker-contrast">Text on this color</h4>
    <div class="ap-color-picker__pairs">
      <div class="ap-color-picker__pair" data-ap-pair data-ink="#FFFFFF">
        <div class="ap-color-picker__sample" aria-hidden="true"><span class="ap-color-picker__aa"></span><span class="ap-color-picker__line"></span></div>
        <p class="ap-color-picker__verdict"><span class="ap-color-picker__who">White text</span> <strong class="ap-color-picker__ratio" translate="no" data-ap-ratio>6.2:1</strong> <span class="ap-color-picker__badge" data-ap-badge><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span data-ap-level>Passes AA</span></span></p>
      </div>
      <div class="ap-color-picker__pair" data-ap-pair data-ink="#000000">
        <div class="ap-color-picker__sample" aria-hidden="true"><span class="ap-color-picker__aa"></span><span class="ap-color-picker__line"></span></div>
        <p class="ap-color-picker__verdict"><span class="ap-color-picker__who">Black text</span> <strong class="ap-color-picker__ratio" translate="no" data-ap-ratio>3.3:1</strong> <span class="ap-color-picker__badge" data-ap-badge><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span data-ap-level>Passes AA</span></span></p>
      </div>
    </div>
    <p class="ap-color-picker__advice" data-ap-advice>Use white text on this color.</p>
  </section>
  <p class="ap-color-picker__sr" role="status" data-ap-say></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.1.1 Non-text Content Level A

    Each swatch has a text name, Indigo or Teal, so a color is never identified only by how it looks.

  • 1.4.1 Use of Color Level A

    The contrast result is a ratio and a verdict in words with an icon; the checked swatch has a ring and a tick.

  • 1.4.11 Non-text Contrast Level AA

    Every swatch has a 1 px outline that clears 3:1 against the card, so light colors like Saffron keep a visible edge.

  • 2.1.1 Keyboard Level A

    The color input opens from the keyboard, the hex field takes typing, and the arrow keys move through the presets.

  • 3.3.1 Error Identification Level A

    A hex code that cannot be read is described in text, tied to the field, and the field is marked invalid.

  • 3.3.2 Labels or Instructions Level A

    The hex field's hint gives the format and an example before anyone types.

  • 4.1.3 Status Messages Level AA

    The new contrast results are announced politely once the color stops changing, not on every step of a drag.

Usage

When to use it

Use it

  • Choosing a brand, theme or label color, where people may have an exact code or just want something close.
  • Settings where the chosen color will carry text, so its contrast matters as much as its hue.

Use something else

  • Choosing from a fixed set of named colors only: a plain radio group is simpler.
  • Precise color work like gradients or print profiles, which needs a full editor with numeric channels.

Common failures

How it usually goes wrong

  • Swatches with no names

    A row of colored squares reads as "radio button, 1 of 8" eight times. Each swatch here has a name, read with it and shown for the checked one.

  • A custom canvas picker with no keyboard

    A drag-only hue wheel locks out keyboard and switch users. The native color input and the hex field cover every case.

  • Hex errors on every keystroke

    Half-typed codes are not mistakes. A full six-character code applies as it is typed; errors, and the three-character short form, wait for Enter or for focus to leave.

  • Contrast shown as a green or red dot

    A dot says nothing to people who cannot tell the two apart. The ratio and verdict are written out, with a tick, a warning or a cross.

  • Rounding a fail into a pass

    4.46:1 rounded to 4.5:1 looks like a pass. The ratio here is rounded down, so a value that shows as passing really passes.

Notes

Building it

  • Relative luminance follows the WCAG 2.2 definition (the sRGB curve with the 0.04045 threshold), and the ratio is (L1 + 0.05) / (L2 + 0.05).
  • Large text only means 3:1 or more, which passes AA for text at least 24 px, or 18.66 px bold.
  • input type="color" always reports lowercase #rrggbb; the script uppercases it for the hex field and accepts either case, with or without #, and the three-digit short form.
  • The swatch colors are set by the script from each radio's value, so the markup carries no inline styles.
  • The samples draw their letters with CSS generated content and are aria-hidden: they illustrate a result that is given in words beside them, and a sample that fails on purpose would otherwise be reported by automated checkers as a page error.

Sources: WCAG 2.2: Contrast ratio · HTML: the color input

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction