Default

Radio group with fieldset and legend.

Choose a plan
<fieldset class="radio-group">
  <legend class="label">Choose a plan</legend>
  <div class="radio-item">
    <input class="radio" type="radio" name="plan" id="free" value="free" checked>
    <label for="free">Free</label>
  </div>
  <div class="radio-item">
    <input class="radio" type="radio" name="plan" id="pro" value="pro">
    <label for="pro">Pro</label>
  </div>
  <div class="radio-item">
    <input class="radio" type="radio" name="plan" id="ent" value="enterprise">
    <label for="ent">Enterprise</label>
  </div>
</fieldset>

With Description

Radio items with label and description text using .radio-item-block.

Spacing
Standard spacing for most use cases.
More space between elements.
Minimal spacing for dense layouts.
<fieldset class="radio-group">
  <legend class="label">Spacing</legend>
  <div class="radio-item-block">
    <input class="radio" type="radio" name="spacing" id="sp-default" value="default" checked>
    <label for="sp-default">Default</label>
    <span class="radio-description">Standard spacing for most use cases.</span>
  </div>
  <div class="radio-item-block">
    <input class="radio" type="radio" name="spacing" id="sp-comfortable" value="comfortable">
    <label for="sp-comfortable">Comfortable</label>
    <span class="radio-description">More space between elements.</span>
  </div>
</fieldset>

Choice Card

Card-style radios using .radio-card with :has() for checked highlight.

Select a plan
<fieldset class="radio-group">
  <legend class="label">Select a plan</legend>
  <label class="radio-card" for="c-plus">
    <input class="radio" type="radio" name="card" id="c-plus" value="plus">
    <label for="c-plus">Plus</label>
    <span class="radio-description">For individuals and small teams.</span>
  </label>
  <label class="radio-card" for="c-pro">
    <input class="radio" type="radio" name="card" id="c-pro" value="pro" checked>
    <label for="c-pro">Pro</label>
    <span class="radio-description">For growing businesses.</span>
  </label>
</fieldset>

Horizontal

Horizontal layout with data-orientation="horizontal".

Alignment
<fieldset class="radio-group" data-orientation="horizontal">
  <legend class="label">Alignment</legend>
  <div class="radio-item">
    <input class="radio" type="radio" name="align" id="left" value="left" checked>
    <label for="left">Left</label>
  </div>
  <div class="radio-item">
    <input class="radio" type="radio" name="align" id="center" value="center">
    <label for="center">Center</label>
  </div>
  <div class="radio-item">
    <input class="radio" type="radio" name="align" id="right" value="right">
    <label for="right">Right</label>
  </div>
</fieldset>

Disabled

Individual or group-level disabling via disabled attribute.

Disabled group
<fieldset class="radio-group">
  <legend class="label">Disabled group</legend>
  <div class="radio-item">
    <input class="radio" type="radio" name="dis" id="d1" disabled checked>
    <label for="d1">Option A</label>
  </div>
  <div class="radio-item">
    <input class="radio" type="radio" name="dis" id="d2" disabled>
    <label for="d2">Option B</label>
  </div>
</fieldset>

Invalid

Validation error state with aria-invalid="true".

Notification Preferences

Choose how you want to receive notifications.

<fieldset class="radio-group">
  <legend class="label">Notification Preferences</legend>
  <p class="radio-group-description">Choose how you want to receive notifications.</p>
  <div class="radio-item">
    <input class="radio" type="radio" name="notif" id="inv1" value="email" aria-invalid="true">
    <label for="inv1">Email only</label>
  </div>
  <div class="radio-item">
    <input class="radio" type="radio" name="notif" id="inv2" value="sms" aria-invalid="true">
    <label for="inv2">SMS only</label>
  </div>
</fieldset>

CSS view file

/* -- Radio Group component -------------------------------------- */

@layer components {
  .radio-group {
    border: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;

    & > legend {
      margin-bottom: 0.375rem;
    }

    /* Horizontal layout */
    &[data-orientation="horizontal"] {
      flex-direction: row;
      flex-wrap: wrap;
      gap: 1rem;
    }
  }

  .radio-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;

    & > label {
      font-size: 0.875rem;
      font-weight: 400;
      cursor: pointer;
    }
  }

  /* -- With-description layout -------------------------------- */
  .radio-item-block {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0 0.5rem;

    & > .radio {
      grid-row: 1 / 3;
      margin-top: 0.125rem;
    }

    & > label {
      font-size: 0.875rem;
      font-weight: 500;
      cursor: pointer;
      line-height: 1.4;
    }

    & > .radio-description {
      grid-column: 2;
      font-size: 0.8125rem;
      color: var(--muted-foreground);
      line-height: 1.5;
    }
  }

  /* -- Card variant ------------------------------------------- */
  .radio-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 1rem 1.25rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    cursor: pointer;
    transition: border-color 150ms, background 150ms;

    &:hover {
      background: var(--accent);
    }

    &:has(.radio:checked) {
      border-color: var(--primary);
    }

    &:has(.radio:focus-visible) {
      outline: 2px solid var(--ring);
      outline-offset: 2px;
    }

    &:has(.radio:disabled) {
      opacity: 0.5;
      cursor: not-allowed;
      pointer-events: none;
    }

    & > .radio {
      position: absolute;
      top: 1rem;
      right: 1rem;
    }

    & > label {
      font-size: 0.875rem;
      font-weight: 500;
      cursor: pointer;
    }

    & > .radio-description {
      font-size: 0.8125rem;
      color: var(--muted-foreground);
      line-height: 1.5;
    }
  }

  .radio {
    appearance: none;
    width: 1rem;
    height: 1rem;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--background);
    cursor: pointer;
    flex-shrink: 0;
    position: relative;
    transition: border-color 150ms;

    &:checked {
      border-color: var(--primary);

      &::after {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        width: 0.5rem;
        height: 0.5rem;
        border-radius: 50%;
        background: var(--primary);
        transform: translate(-50%, -50%);
      }
    }

    &:focus-visible {
      outline: 2px solid var(--ring);
      outline-offset: 2px;
    }

    &:disabled {
      opacity: 0.5;
      cursor: not-allowed;

      & + label { opacity: 0.5; cursor: not-allowed; }
    }

    &[aria-invalid="true"] {
      border-color: var(--destructive);

      &:checked {
        border-color: var(--destructive);

        &::after {
          background: var(--destructive);
        }
      }
    }
  }

  /* Helper text for group-level descriptions */
  .radio-group-description {
    font-size: 0.8125rem;
    color: var(--muted-foreground);
    margin: -0.125rem 0 0.25rem;
  }

  /* -- Accessibility ------------------------------------------ */
  @media (prefers-reduced-motion: reduce) {
    .radio,
    .radio-card {
      transition: none;
    }
  }

  @media (forced-colors: active) {
    .radio {
      border-color: ButtonText;

      &:checked {
        border-color: Highlight;

        &::after {
          background: Highlight;
        }
      }

      &:disabled {
        border-color: GrayText;

        &:checked::after {
          background: GrayText;
        }
      }
    }

    .radio-card {
      border-color: ButtonText;

      &:has(.radio:checked) {
        border-color: Highlight;
      }
    }
  }
}