Navigation

v0.0.4
Forms

Swatch

A color to look at or to pick, as a disc with a finished edge.

On this page

A swatch is a color as a thing to look at or to pick. Set the color with --swatch. It is a small disc with a hairline just inside its edge and a line of light on its top, so white reads on white and black on black. As a <label> around a native radio it is a choice: the chosen one stands in a ring of ink, a gap away.

Swatch

<div class="flex gap-md" role="radiogroup" aria-label="Color">  <label class="swatch" style="--swatch: oklch(24% .004 265)">    <input class="swatch-input" type="radio" name="color" aria-label="Graphite">  </label>  <label class="swatch" style="--swatch: oklch(97% .002 265)">    <input class="swatch-input" type="radio" name="color" aria-label="Pearl" checked>  </label>  <label class="swatch" style="--swatch: oklch(58% .2 280)">    <input class="swatch-input" type="radio" name="color" aria-label="Indigo">  </label>  <label class="swatch" style="--swatch: oklch(66% .14 150)">    <input class="swatch-input" type="radio" name="color" aria-label="Moss">  </label>  <label class="swatch" style="--swatch: oklch(74% .15 70)">    <input class="swatch-input" type="radio" name="color" aria-label="Amber">  </label>  <label class="swatch" style="--swatch: oklch(66% .18 25)">    <input class="swatch-input" type="radio" name="color" aria-label="Coral" disabled>  </label></div>

The gap between a chosen swatch and its ring is cut in the color of the surface it sits on. Surfaces publish that color as --surface-color, so the same markup is right on the page, on a sheet and in a popover.

#Several at once

With checkboxes, any number can be chosen. The ring is the state; a swatch carries no check, since no one glyph reads on every color.

Several at once

<div class="sheet flex gap-md p-md" role="group" aria-label="Labels">  <label class="swatch swatch-lg" style="--swatch: oklch(58% .2 280)">    <input class="swatch-input" type="checkbox" name="label" aria-label="Indigo" checked>  </label>  <label class="swatch swatch-lg" style="--swatch: oklch(66% .14 150)">    <input class="swatch-input" type="checkbox" name="label" aria-label="Moss">  </label>  <label class="swatch swatch-lg" style="--swatch: oklch(74% .15 70)">    <input class="swatch-input" type="checkbox" name="label" aria-label="Amber" checked>  </label>  <label class="swatch swatch-lg" style="--swatch: oklch(66% .18 25)">    <input class="swatch-input" type="checkbox" name="label" aria-label="Coral">  </label></div>

#Sizes and shape

A swatch is 24px; swatch-sm is 16px, for a row in a list or a menu, and swatch-lg 32px. swatch-square is a tile with a chip's corner, for a palette or a token table. Without an input a swatch is a plain mark: put it beside the color's name.

Sizes and shape

<div class="flex items-center gap-sm">  <span class="swatch swatch-sm" style="--swatch: oklch(58% .2 280)"></span>  <span class="swatch" style="--swatch: oklch(58% .2 280)"></span>  <span class="swatch swatch-lg" style="--swatch: oklch(58% .2 280)"></span>  <span class="swatch swatch-lg swatch-square" style="--swatch: oklch(58% .2 280)"></span>  <span class="swatch swatch-lg swatch-square" style="--swatch: linear-gradient(135deg, oklch(86% .07 280), oklch(88% .07 350))"></span></div><div class="flex items-center gap-xs text-sm fg-text-strong">  <span class="swatch swatch-sm" style="--swatch: oklch(66% .14 150)"></span>Moss  <span class="fg-text-muted">oklch(66% .14 150)</span></div>

#Accessibility

  • A color has no text, so every input needs a name: aria-label="Indigo" on the input, or visible text beside the swatch tied with aria-labelledby. Name the color, not its use, unless the use is the point ("Danger").
  • Group the choices in a role="radiogroup" or a <fieldset> with a label. Radios that share a name move with the arrow keys.
  • The chosen state is a ring of ink, not a change of color, and keyboard focus draws its own outline outside that ring.
  • On touch screens the target grows to 44px around the disc without moving it; leave at least 12px between swatches.

#Classes

ClassDescription
swatchThe disc, 24px wide: a label around a swatch-input, or a plain mark
swatch-inputThe radio or checkbox inside a swatch; it covers the disc invisibly and needs a name, since a color has no text
swatch-sm16px; the default is 24px. With swatch
swatch-lg32px; the default is 24px. With swatch
swatch-squareA tile with a chip's corner, for a palette or a token table. With swatch

#Custom properties

PropertyDefaultDescription
--swatchcurrentColorThe color, or any background image
--swatch-size1.5remThe diameter

#Source

@master/ui/components/swatch.css
/* * Swatch: a color as a thing to look at or to pick. Set the color with --swatch. * A swatch is a small disc with a hairline inside its edge, so white reads on white and black on black. * As a label around a radio or a checkbox it is a choice: the chosen one stands in a ring of ink, * a gap away, cut from the surface it sits on. */@layer components {  .swatch {    --swatch: currentColor;    --swatch-size: 1.5rem;    position: relative;    display: inline-block;    flex: none;    width: var(--swatch-size);    height: var(--swatch-size);    margin: 0;    border-radius: 999px;    background: var(--swatch);    /* An edge inside the disc and a line of light on its top, whatever the color. */    box-shadow: inset 0 0 0 .5px var(--color-line-on-color), inset 0 1px 0 var(--color-highlight-on-color);    vertical-align: middle;    -webkit-tap-highlight-color: transparent;    &:has(.swatch-input) {      cursor: pointer;      touch-action: manipulation;      transition-property: box-shadow, scale;      transition-duration: var(--duration-quick);      transition-timing-function: var(--easing-glide);    }    @media (hover: hover) {      &:has(.swatch-input:not(:disabled, :checked)):hover {        scale: 1.08;      }    }    &:has(.swatch-input:not(:disabled)):active {      scale: .94;      transition-duration: var(--duration-press);    }    /* Chosen: a 2px gap in the surface's own color, then a ring of ink. */    &:has(.swatch-input:checked) {      box-shadow:        inset 0 0 0 .5px var(--color-line-on-color), inset 0 1px 0 var(--color-highlight-on-color),        0 0 0 2px var(--surface-color, var(--color-surface-base)), 0 0 0 3.5px var(--color-text-strong);    }    &:has(.swatch-input:focus-visible) {      @apply --focus-ring;      outline-offset: 5px;    }    &:has(.swatch-input:disabled) {      opacity: .4;      cursor: not-allowed;    }    /* Square: a tile with a chip's corner, for a palette or a token table. */    &.swatch-square {      border-radius: calc(var(--radius-chip) * var(--corner-scale));      corner-shape: var(--corner-shape);    }    &.swatch-sm { --swatch-size: 1rem; }    &.swatch-lg { --swatch-size: 2rem; }    @media (pointer: coarse) {      /* The disc keeps its size; the target around it grows to 44px. */      &:has(.swatch-input)::after {        content: '';        position: absolute;        inset: calc((var(--swatch-size) - 2.75rem) / 2);      }    }    @media (prefers-reduced-motion: reduce) {      &:has(.swatch-input) {        scale: none;      }    }    @media (forced-colors: active) {      outline: 1px solid CanvasText;      forced-color-adjust: none;      &:has(.swatch-input:checked) {        outline: 3px solid Highlight;        outline-offset: 2px;      }    }  }  /* The radio or checkbox covers the swatch invisibly: the label is the target, the input keeps the state. */  .swatch-input {    position: absolute;    inset: 0;    width: 100%;    height: 100%;    margin: 0;    opacity: 0;    cursor: inherit;    appearance: none;  }}