Skip to content

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 Ocean

Code

The following example assumes that you have imported the component and set up the theme.

Color Swatches
<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>