Skip to content

Radio


vue
<template>
  <PeachyInput.Input>
    <PeachyRadio.Input v-model:selected="selected">
      <PeachyRadio.InputLabel>Radio group</PeachyRadio.InputLabel>

      <div class="radio-wrapper">
        <PeachyRadio.Radio value="1">
          <template #after>
            <PeachyRadio.Indicator />
            <PeachyRadio.Label>Radio 1</PeachyRadio.Label>
          </template>
        </PeachyRadio.Radio>
      </div>

      <div class="radio-wrapper">
        <PeachyRadio.Radio value="2">
          <template #after>
            <PeachyRadio.Indicator />
            <PeachyRadio.Label>Radio 2</PeachyRadio.Label>
          </template>
        </PeachyRadio.Radio>
      </div>
    </PeachyRadio.Input>
  </PeachyInput.Input>
</template>
css
.peachy-radio__input {
  display: grid;
  gap: var(--gap);
  border: none;
}

.peachy-radio__input-label {
  min-width: max-content;
  padding-bottom: 0.5rem;
  font-weight: var(--fw-bold);
}

.radio-wrapper {
  display: flex;
  align-items: center;
  gap: var(--gap);
  position: relative;
  cursor: pointer;

  width: fit-content;
  padding: 0 1rem 0 0;
}

.peachy-radio__label {
  cursor: pointer;
}

.peachy-radio:focus-visible {
  outline: none !important;
}

.peachy-radio {
  width: 1.25rem;
  height: 1.25rem;

  position: absolute;
}

.peachy-radio:has(:focus-visible) {
  outline: 2px solid var(--brand-color);
  outline-offset: 2px;
  border-radius: 99rem;
}

.peachy-radio__indicator[data-checked="false"] {
  --radio-color: var(--dimmed-text);
}

.peachy-radio__indicator[data-checked="true"] {
  --radio-color: var(--brand-color-dimmed);
}

.peachy-radio__indicator:hover,
.peachy-radio:hover + .peachy-radio__indicator {
  --radio-color: var(--text);
}

.peachy-radio__indicator[data-checked="true"]:hover,
.peachy-radio:hover + .peachy-radio__indicator[data-checked="true"],
.peachy-radio:focus + .peachy-radio__indicator {
  --radio-color: var(--brand-color);
}

.peachy-radio__indicator {
  transition: border 0.25s;

  width: 1.25rem;
  height: 1.25rem;

  border: 2px solid var(--radio-color);
  border-radius: 100%;
  padding: 0.175rem;

  position: relative;
}

.peachy-radio__indicator[data-checked="true"]::before {
  content: "";
  position: absolute;
  inset: 0.15rem;

  background-color: var(--radio-color);
  border-radius: 100%;

  transition:
    background-color 0.25s,
    transform 0.25s;
}

@media (prefers-reduced-motion: no-preference) {
  .peachy-radio__indicator[data-checked="true"]::before {
    animation: ScaleIn 0.25s;
  }
}

/** ===== Animation ===== */

@keyframes ScaleIn {
  from {
    transform-origin: center;
    transform: scale(0.6);
  }
  to {
    transform: scale(1);
  }
}

Anatomy

Radio does not use <PeachyInput.Label />, but rather InputLabel for the group, and Label for individual radios.

vue
<template>
  <PeachyInput.Input>
    <PeachyRadio.Input>
      <template #before />

      <PeachyRadio.InputLabel />

      <PeachyRadio.Radio>
        <PeachyRadio.Indicator />
        <PeachyRadio.Label />
      </PeachyRadio.Radio>

      <template #after />
    </PeachyRadio.Input>
  </PeachyInput.Input>
</template>

<script lang="ts" setup>
  import { PeachyRadio } from "typeach";
</script>

Indicator has a slot, which only renders when the radio is checked.

Props & Emits

Input


Props

NameDefaultType
selectedstring?
disabledfalseboolean?
readOnlyfalseboolean?

Emits

@Payload
update:selectedstring
validatestring
clear-validation

Radio


Props

NameDefaultType
valuestring
disabledfalseboolean?
readOnlyfalseboolean?

Styling

INPUT HAS DEFAULT STYLING

The Input element has a style of appearance: none, to allow styling it yourself. You should however style the input and Indicator to be aligned, so that the click-area lines up (see the example).

CSS Selectors

Follows our CSS classes convention.


State selectors

SelectorDescriptionFor
:checkedFor a checked radio.
  • Input
:disabledFor a disabled radio.
  • Input
[data-readonly="<boolean>"]For a read-only radio.
  • Input
[data-checked="<boolean>"]For the checked state.
  • Indicator

Accessibility

Resources: One last time: custom styling radio buttons and checkboxes

READONLY

Native radio inputs do not offer a readonly state, and aria-readonly has spotty support. So setting the radios to readonly makes their input disabled with aria-disabledto keeps it in the tab order, and manually prevents the input from being edited.

There is some disagreements if disabled conveys the right intent as a replacement for readonly, however, usually the solution people arrive at when they want a readonly radio is to make it disabled - so I figure this is a reasonable compromise.

References: Why is aria-readonly allowed for checkboxes and radios?, Provide a way to have disabled form controls to be submitted (was: readonly attribute)

Keyboard interactions

KeyAction
Moves focus to the previous radio and selects it.
Moves focus to the next radio and selects it.