Radio Color Swatches
A product color picker where each radio is the color itself. A check slotted into dot draws itself in from :state(--checked), and aria-label names each swatch.
Color
Code
The following example assumes that you have imported the component and set up the theme.
<div class="swatch-picker">22 collapsed lines
<div class="swatch-header"> <span>Color</span> <output class="swatch-name">Ocean</output> </div>
<div class="swatches" aria-label="Color" role="radiogroup"> <sv-radio class="swatch" style="--swatch: #2563eb" aria-label="Ocean" checked name="color" value="ocean"> <svg class="check" aria-hidden="true" slot="dot" viewBox="0 0 24 24"><path d="M6 12.5l4 4L18 8.5" pathLength="1" /></svg> </sv-radio> <sv-radio class="swatch" style="--swatch: #d6b47a" aria-label="Sand" name="color" value="sand"> <svg class="check" aria-hidden="true" slot="dot" viewBox="0 0 24 24"><path d="M6 12.5l4 4L18 8.5" pathLength="1" /></svg> </sv-radio> <sv-radio class="swatch" style="--swatch: #15803d" aria-label="Forest" name="color" value="forest"> <svg class="check" aria-hidden="true" slot="dot" viewBox="0 0 24 24"><path d="M6 12.5l4 4L18 8.5" pathLength="1" /></svg> </sv-radio> <sv-radio class="swatch" style="--swatch: #e11d48" aria-label="Rose" name="color" value="rose"> <svg class="check" aria-hidden="true" slot="dot" viewBox="0 0 24 24"><path d="M6 12.5l4 4L18 8.5" pathLength="1" /></svg> </sv-radio> <sv-radio class="swatch" style="--swatch: #374151" aria-label="Graphite" name="color" value="graphite"> <svg class="check" aria-hidden="true" slot="dot" viewBox="0 0 24 24"><path d="M6 12.5l4 4L18 8.5" pathLength="1" /></svg> </sv-radio> </div></div>
<script type="application/x-typescript" module>6 collapsed lines
const picker = document.querySelector<HTMLElement>(".swatch-picker")!; const colorName = picker.querySelector<HTMLOutputElement>(".swatch-name")!;
picker.addEventListener("change", event => { colorName.value = (event.target as HTMLElement).ariaLabel!; });</script>
<style>63 collapsed lines
.swatch-picker { display: grid; gap: 0.8em; justify-content: center; padding-block: 1.5em;
.swatch-header { display: flex; gap: 0.5em; font-size: 0.9em;
span { color: var(--sv-gray-3); }
output { font-weight: 600; } }
.swatches { display: flex; gap: 0.9em; }
.swatch { --sv-radio-sz: 2em; --sv-radio-bg: var(--swatch); --sv-radio-bdr-clr: var(--swatch); --sv-radio-active-bdr-clr: color-mix(in srgb, var(--swatch), light-dark(white, black) 50%); --sv-radio-active-bdr-sz: 4px; }
.check { inline-size: 55%; block-size: 55%; overflow: visible; filter: drop-shadow(0 1px 1px rgb(0 0 0 / 35%)); fill: none; stroke: #fff; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
path { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 120ms ease-in; } }
.swatch:state(--checked) .check path { stroke-dashoffset: 0; transition: stroke-dashoffset 300ms cubic-bezier(0.65, 0, 0.35, 1) 60ms; }
@media (prefers-reduced-motion: reduce) { .check path, .swatch:state(--checked) .check path { transition: none; } } }</style>