CSS · 03 of 05

The parent gets the message

Style a card from its selected input. Native radios still own the interaction.

:has()Baseline Widely availableMDN :has()
Experience kept
Native selection, labels, and keyboard behavior
Machinery removed
Mirrored classes and a change listener
Decision
Style the state the form already owns
Live demo

Choose an option. Try the arrow keys.

THE REDUCTION

Same outcome. Less to coordinate.

A class mirroring a checked radio is a second copy of state the form already holds.

CURRENT PROJECT · JS
group.addEventListener("change", () => {
  for (const option of group.querySelectorAll(".option")) {
    const input = option.querySelector("input");
    option.classList.toggle("is-selected", input.checked);
  }
});
WITH FEWER PARTS · CSS
.option:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-surface);
}

The listener goes. Arrow-key selection, labels, and the checked state stay exactly where they were, in the native radio group.

ABOUT THE PATTERN

Why this works

The parent can reflect real form state directly, removing the JavaScript that would otherwise synchronize a class.

Decisions that matter

  • Native radios retain selection, labels, and keyboard behavior.
  • CSS changes the parent surface; no duplicated application state.
  • Visible radio indicators remain useful without :has().

Read from the skill's own reference files at build time.

COPY, ADAPT, SHIP

The dependency-free implementation

HTML, CSS, and the JavaScript the interaction actually needs

STANDALONE HTML
<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Selection-aware cards</title>
<style id="recipe">
.option:has(input:checked) {
  border-color: #3866e8;
  background: #edf3ff;
}
.option:has(input:focus-visible) {
  outline: 3px solid #3866e8;
  outline-offset: 3px;
}
</style>
<style>
*{box-sizing:border-box}body{margin:0;padding:28px;font:14px/1.5 system-ui;color:#182334;background:#f2f5f9}fieldset{border:0;padding:0;margin:0}legend{margin-bottom:16px;font-weight:600}.option{display:flex;align-items:center;gap:14px;padding:16px;margin-bottom:12px;border:1px solid #ccd5e2;border-radius:12px;background:white;cursor:pointer}.option span{flex:1}.option strong{display:block;font-weight:600}.option small{font-size:13px;color:#647186}input{accent-color:#3866e8;width:18px;height:18px}input:focus-visible{outline:2px solid #3866e8;outline-offset:2px}
</style>
<fieldset><legend>Choose your workspace</legend>
<label class="option"><input type="radio" name="workspace" value="personal" checked><span><strong>Personal</strong><small>A little space for your ideas</small></span></label>
<label class="option"><input type="radio" name="workspace" value="team"><span><strong>Team</strong><small>Bring everyone together</small></span></label>
</fieldset>
<!-- Native radios own selection and keyboard behavior. No JavaScript needed. -->
</html>
Using Tailwind CSS v4 already? View the adapted example

Optional integration · adapt utilities to your project tokens

TAILWIND HTML
<!-- Tailwind CSS v4. Native radios keep selection and keyboard behavior. -->
<fieldset class="m-0 border-0 p-0">
  <legend class="mb-4 font-semibold text-slate-900">Choose your workspace</legend>

  <label class="mb-3 flex cursor-pointer items-center gap-4 rounded-xl border border-slate-300 bg-white p-4 has-checked:border-blue-600 has-checked:bg-blue-50 has-[input:focus-visible]:outline-3 has-[input:focus-visible]:outline-offset-3 has-[input:focus-visible]:outline-blue-600">
    <input type="radio" name="workspace" value="personal" checked class="size-5 accent-blue-600">
    <span class="min-w-0 flex-1">
      <strong class="block font-semibold text-slate-900">Personal</strong>
      <small class="text-sm text-slate-600">A little space for your ideas</small>
    </span>
  </label>

  <label class="flex cursor-pointer items-center gap-4 rounded-xl border border-slate-300 bg-white p-4 has-checked:border-blue-600 has-checked:bg-blue-50 has-[input:focus-visible]:outline-3 has-[input:focus-visible]:outline-offset-3 has-[input:focus-visible]:outline-blue-600">
    <input type="radio" name="workspace" value="team" class="size-5 accent-blue-600">
    <span class="min-w-0 flex-1">
      <strong class="block font-semibold text-slate-900">Team</strong>
      <small class="text-sm text-slate-600">Bring everyone together</small>
    </span>
  </label>
</fieldset>