Color Option

Color: Teal
@use 'sprucecss/scss/spruce' as *;

.form-color-option {
  @include layout-stack('xs');
  border: 0;
  margin: 0;
  min-inline-size: 0;
  padding: 0;

  &__label {
    color: color('label', 'form');
    font-family: config('font-family', $form-label, false);
    font-size: config('font-size', $form-label, false);
    font-weight: config('font-weight', $form-label, false);
    padding: 0;
  }

  &__selected {
    color: color('muted');
    font-weight: 400;
  }

  &__inner {
    display: flex;
    flex-wrap: wrap;
    gap: spacer('s');
  }
}

.form-color {
  $this: &;
  --color: currentcolor;
  --size: 2.5rem;
  display: inline-flex;

  &--check-light {
    #{$this}__control:checked + #{$this}__label::before {
      @include field-icon(config('checkbox', $form-icon, false), white);
    }
  }

  &--check-dark {
    #{$this}__control:checked + #{$this}__label::before {
      @include field-icon(config('checkbox', $form-icon, false), black);
    }
  }

  &__control {
    @include visually-hidden;

    &:checked + #{$this}__label {
      box-shadow: 0 0 0 2px color('background'), 0 0 0 4px var(--color);

      &::before {
        --size: 1.25rem;
        background-size: var(--size);
        block-size: var(--size);
        content: '';
        inline-size: var(--size);
      }
    }

    &:focus-visible + #{$this}__label {
      outline: 2px solid color('focus-ring');
      outline-offset: 6px;
    }

    // Sold out: a diagonal line over a faded swatch.
    &:disabled + #{$this}__label {
      background-image: linear-gradient(to top right, transparent calc(50% - 1px), color('background') calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px));
      cursor: not-allowed;
      opacity: 0.4;
    }
  }

  &__label {
    align-items: center;
    background-color: var(--color);
    block-size: var(--size);
    border-radius: 50%;
    box-shadow: inset 0 0 0 1px hsl(0deg 0% 0% / 15%);
    cursor: pointer;
    display: inline-flex;
    inline-size: var(--size);
    justify-content: center;
  }
}
<fieldset class="form-color-option" data-option-group>
    <legend class="form-color-option__label">
        Color: <span class="form-color-option__selected" data-option-selected>Teal</span>
    </legend>
    <div class="form-color-option__inner">
        <label class="form-color form-color--check-dark" style="--color: hsl(340deg 82% 76%);" title="Rose">
            <input class="form-color__control" type="radio" value="rose" name="color" aria-label="Rose" data-label="Rose"/>
            <span class="form-color__label"></span>
        </label>
        <label class="form-color form-color--check-light" style="--color: hsl(282deg 44% 42%);" title="Plum">
            <input class="form-color__control" type="radio" value="plum" name="color" aria-label="Plum" data-label="Plum"/>
            <span class="form-color__label"></span>
        </label>
        <label class="form-color form-color--check-light" style="--color: hsl(180deg 60% 32%);" title="Teal">
            <input class="form-color__control" type="radio" value="teal" name="color" aria-label="Teal" data-label="Teal" checked/>
            <span class="form-color__label"></span>
        </label>
        <label class="form-color form-color--check-dark" style="--color: hsl(16deg 100% 66%);" title="Coral">
            <input class="form-color__control" type="radio" value="coral" name="color" aria-label="Coral" data-label="Coral"/>
            <span class="form-color__label"></span>
        </label>
        <label class="form-color form-color--check-dark" style="--color: hsl(240deg 67% 90%);" title="Lavender (sold out)">
            <input class="form-color__control" type="radio" value="lavender" name="color" aria-label="Lavender (sold out)" data-label="Lavender" disabled/>
            <span class="form-color__label"></span>
        </label>
        <label class="form-color form-color--check-light" style="--color: hsl(220deg 8% 25%);" title="Graphite">
            <input class="form-color__control" type="radio" value="graphite" name="color" aria-label="Graphite" data-label="Graphite"/>
            <span class="form-color__label"></span>
        </label>
    </div>
</fieldset>
document.querySelectorAll('[data-option-group]').forEach((group) => {
  const output = group.querySelector('[data-option-selected]');

  // Show the label of the checked option next to the legend.
  group.addEventListener('change', (event) => {
    output.textContent = event.target.dataset.label;
  });
});

Color Option

A color swatch picker for product variants: native radio buttons styled as swatches, with the color name next to the legend and sold-out colors crossed out.

Anatomy

  • .form-color-option is a fieldset; the legend names the group and shows the selected color (.form-color-option__selected).
  • .form-color is one swatch: a visually hidden radio input and its visible circle (.form-color__label). Set the color with the --color custom property.
  • .form-color--check-light and .form-color--check-dark set the color of the check mark; pick the one that contrasts with the swatch.

Accessibility

  • A swatch has no visible text, so every input has an aria-label with the color name (and "(sold out)" when it's disabled). The title gives the same name as a tooltip on hover.
  • The selected swatch gets a check mark and a double ring, so the state isn't communicated by color only.
  • Native radio inputs keep the keyboard support: the arrow keys move between the colors.
  • Each swatch has an inset hairline, so light colors (like Lavender) stay visible on a light background.

Technical Details

  • The script only updates the selected name next to the legend (from data-label); without JavaScript the control still works.
  • The check mark is the checkbox icon of the Spruce form config ($form-icon), drawn with the field-icon mixin.

Customization

<label class="form-color form-color--check-light" style="--color: hsl(140deg 50% 35%);">
    <input class="form-color__control" type="radio" name="color" value="forest" aria-label="Forest" data-label="Forest">
    <span class="form-color__label"></span>
</label>

Change the size with --size on .form-color.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.