Size Option

Size: M

Size guide

@use 'sprucecss/scss/spruce' as *;

.form-size-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('xs');
  }

  &__guide {
    font-size: config('font-size-sm', $typography, false);
  }
}

.form-size {
  $this: &;
  display: flex;
  position: relative;

  &__control {
    @include visually-hidden;

    &:checked + #{$this}__label {
      background-color: color('primary-background', 'btn');
      border-color: color('primary-background', 'btn');
      color: color('primary-foreground', 'btn');
    }

    &:focus-visible + #{$this}__label {
      @include short-ring('button', 'primary');
    }

    // Sold out: crossed out, still readable.
    &:disabled + #{$this}__label {
      background-image: linear-gradient(to top right, transparent calc(50% - 1px), color('border') calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px));
      color: color('muted');
      cursor: not-allowed;
    }
  }

  &__label {
    @include transition($property: 'background-color, border-color, color');
    align-items: center;
    background-color: color('background', 'form');
    border: config('border-width-base', $display) solid color('border', 'form');
    border-radius: config('border-radius', $btn, false);
    color: color('label', 'form');
    cursor: pointer;
    display: flex;
    font-size: config('font-size-sm', $typography, false);
    font-weight: 700;
    justify-content: center;
    min-block-size: 2.75rem;
    min-inline-size: 2.75rem;
    padding-inline: spacer('xs');
  }

  &:hover &__control:not(:checked, :disabled) + &__label {
    border-color: color('primary');
  }
}
<fieldset class="form-size-option" data-option-group>
    <legend class="form-size-option__label">
        Size: <span class="form-size-option__selected" data-option-selected>M</span>
    </legend>
    <div class="form-size-option__inner">
        <label class="form-size">
            <input class="form-size__control" type="radio" value="xs" name="size" data-label="XS"/>
            <span class="form-size__label">XS</span>
        </label>
        <label class="form-size">
            <input class="form-size__control" type="radio" value="s" name="size" data-label="S" disabled/>
            <span class="form-size__label">S</span>
            <span class="sr-only">(sold out)</span>
        </label>
        <label class="form-size">
            <input class="form-size__control" type="radio" value="m" name="size" data-label="M" checked/>
            <span class="form-size__label">M</span>
        </label>
        <label class="form-size">
            <input class="form-size__control" type="radio" value="l" name="size" data-label="L"/>
            <span class="form-size__label">L</span>
        </label>
        <label class="form-size">
            <input class="form-size__control" type="radio" value="xl" name="size" data-label="XL"/>
            <span class="form-size__label">XL</span>
        </label>
        <label class="form-size">
            <input class="form-size__control" type="radio" value="xxl" name="size" data-label="XXL" disabled/>
            <span class="form-size__label">XXL</span>
            <span class="sr-only">(sold out)</span>
        </label>
    </div>
    <p class="form-size-option__help"><a class="form-size-option__guide" href="#">Size guide</a></p>
</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;
  });
});

Size Option

A size picker for product pages: native radio buttons styled as tiles, with the current choice next to the legend and crossed-out sold-out sizes.

Anatomy

  • .form-size-option is a fieldset; its legend names the group and shows the selected size (.form-size-option__selected).
  • .form-size is one option: a visually hidden radio input (.form-size__control) and its visible tile (.form-size__label).
  • .form-size-option__guide is an optional link to a size guide, wrapped in a p (__help): a link is an inline element, so the parent stack’s top margin only works on a block around it.

Accessibility

  • Native radio inputs keep the expected keyboard support: Tab moves into the group, the arrow keys change the selection.
  • The fieldset and legend give the group its name; screen readers announce "Size, M, radio button, 3 of 6".
  • Sold-out sizes are disabled and get a visually hidden "(sold out)" note, so the state isn't communicated by the crossed-out style only.
  • The focus ring uses the button focus ring of Spruce (short-ring).

Technical Details

  • The script only updates the selected label (from the data-label attribute); the control works without JavaScript, and the form submits the value as usual.
  • The tiles are at least 2.75rem large, which keeps them easy to tap.
  • The colors come from the form and button tokens, so it follows the dark mode and every style.

Customization

Use any labels (shoe sizes, capacities, plans): the tiles grow with their content. To change the shape, override the border-radius of .form-size__label.

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.