ATOM
Components (registry)Forms

Radio

Picks exactly one option in a group; the previous choice clears. Mutually exclusive settings.

Preview

Editorial

<!-- F12c editorial — non-derivable only. Review: Karen. -->

## Ejemplos

Mutually exclusive plan:

```tsx import { Radio } from '@/components/atoms/Radio';

<fieldset> <legend>Billing period</legend> <Radio name="period" label="Monthly" checked={period === 'm'} onChange={() => setPeriod('m')} /> <Radio name="period" label="Yearly" checked={period === 'y'} onChange={() => setPeriod('y')} /> </fieldset> ```

## Accesibilidad

- Share the same `name` across options in a group; prefer a `fieldset` + `legend` (or `Field`) for the group name. - Built-in `label` associates text with the control; do not rely on placeholder text alone.

### Correcto

- Input nativo `<input type='radio'>` — screen readers lo leen como radio button - name compartido entre opciones del mismo grupo para exclusion mutua nativa - Touch target 40x40px cumple WCAG - Arrow keys navegan entre opciones del mismo name (comportamiento nativo) - Label clickeable — click en el texto selecciona la opcion - focus-visible ring solo por teclado

### Evitar

- No usar Radio sin name — las opciones no seran mutuamente excluyentes - No usar Radio para toggles on/off — usar Checkbox o Toggle en su lugar - No usar un solo Radio — siempre son 2+ opciones en un grupo

## Cuándo no usar

- Multi-select → `Checkbox` / `ToggleGroup type="multiple"`. - On/off for a single preference → `Toggle` / one `Checkbox`.

## Criterio de uso

- Úsalo cuando las opciones son mutuamente excluyentes y el usuario necesita comparar alternativas antes de elegir una. - Muestra todas las opciones relevantes cuando el conjunto es pequeño; para listas largas considera `Select` o `Combobox`. - Mantén una opción seleccionada cuando el formulario exige una elección y valida el grupo como una unidad.

## Gotchas

- Todos los radios del grupo comparten `name` y deben vivir bajo un `fieldset` con `legend` o un label equivalente. - No dependas del placeholder ni de una diferencia de color para comunicar la opción seleccionada. - **Nota**: La unica diferencia visual con Checkbox: border-radius 9999px (circular vs 4px cuadrado) y dot interno con scale animation en vez de check/minus SVG icon. - **Nota**: CSS autocontenido. La animacion del dot (scale 0→1) es puro CSS via :checked selector — no necesita JS para la transicion.

Uso

import { Radio, RadioGroup } from '@/components/atoms/Radio';

<RadioGroup value={v} onValueChange={setV}>
  <Radio value="a" label="A" />
  <Radio value="b" label="B" />
</RadioGroup>

Props

  • checked: boolean
  • disabled: boolean
  • error: boolean
  • label: string
  • onChange: (checked: boolean) => void

Gotchas

  • a11y

    Group radios with the same name and a fieldset/legend or Field group label.

Anatomía CSS

<div class="radio">
  <span class="radio__circle"></span>
  <span class="radio__dot"></span>
  <span class="radio__input"></span>
  <span class="radio__label"></span>
</div>
ClasePropósito
radioroot
radio--disabledmodifier
radio--errormodifier
radio__circleelement
radio__dotelement
radio__inputelement
radio__labelelement

Tokens resueltos

Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.

VariantePropValorToken
allbg#0a0a0acheckbox.bg.checked
allborder#0a0a0acheckbox.bg.checked
allhover-bg#262626checkbox.bg.checked.hover
alldisabled-bg#525252checkbox.fg.disabled
alldisabled-border#f5f5f5checkbox.bg.disabled
allerror-border#f84131checkbox.border.error
allfg#0a0a0aforeground
alldisabled-fg#525252muted.foreground

Animaciones

PropiedadDuraciónEasing
background-colorvar(--duration-150)var(--easing-out)
border-colorvar(--duration-150)var(--easing-out)
box-shadowvar(--duration-150)var(--easing-out)
transformvar(--duration-150)var(--easing-out)

CSS mínimo funcional

Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).

/* -------------------------------------------------------------------------
   Radio

   Single size (16px circle, 40px touch target).
   Native <input type="radio"> hidden, visual replacement via label.
   Shares color tokens with Checkbox (same palette).

   States:  unchecked, checked, disabled, error
   ------------------------------------------------------------------------- */

.radio {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  position: relative;
}

.radio--disabled {
  cursor: not-allowed;
  pointer-events: none;
}

/* Hidden native input with touch target */
.radio__input {
  position: absolute;
  width: 2.5rem;
  height: 2.5rem;
  margin: 0;
  padding: 0;
  opacity: 0;
  cursor: inherit;
  left: -0.75rem;
  top: 50%;
  transform: translateY(-50%);
}

/* Visual circle */
.radio__circle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  border-radius: 9999px;
  border: 1.5px solid #525252;
  background-color: #fafafa;
  transition:
    background-color 150ms cubic-bezier(0.22, 1, 0.36, 1),
    border-color 150ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Inner dot */
.radio__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 9999px;
  background-color: #fafafa;
  transform: scale(0);
  transition: transform 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---- Hover ---- */

.radio__input:hover:not(:disabled) ~ .radio__circle {
  background-color: #f5f5f5;
}

.radio__input:checked:hover:not(:disabled) ~ .radio__circle {
  background-color: #262626;
}

/* ---- Checked ---- */

.radio__input:checked ~ .radio__circle {
  background-color: #0a0a0a;
  border-color: #0a0a0a;
}

.radio__input:checked ~ .radio__circle .radio__dot {
  transform: scale(1);
}

/* ---- Focus ---- */

.radio__input:focus-visible ~ .radio__circle {
  box-shadow: 0 0 0 2px #a1a1a1b3;
}

/* ---- Disabled ---- */

.radio__input:disabled ~ .radio__circle {
  background-color: #f5f5f5;
  border-color: #f5f5f5;
}

.radio__input:disabled ~ .radio__circle .radio__dot {
  background-color: #525252;
}

/* ---- Error ---- */

.radio--error .radio__circle {
  border-color: #f84131;
}

/* ---- Label ---- */

.radio__label {
  font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
  font-size: 12.8px;
  line-height: 1;
  color: #0a0a0a;
}

.radio--disabled .radio__label {
  color: #525252;
}

Codigo fuente

components-react
components/atoms/Radio.tsx
import { forwardRef, type InputHTMLAttributes } from 'react';

export type RadioProps = {
  checked?: boolean;
  disabled?: boolean;
  error?: boolean;
  label?: string;
  className?: string;
  onChange?: (checked: boolean) => void;
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'checked' | 'onChange'>;

function cn(...classes: (string | false | undefined | null)[]) {
  return classes.filter(Boolean).join(' ');
}

export const Radio = forwardRef<HTMLInputElement, RadioProps>(
  (
    {
      checked = false,
      disabled = false,
      error = false,
      label,
      className,
      onChange,
      ...props
    },
    ref,
  ) => {
    const classes = cn(
      'radio',
      disabled && 'radio--disabled',
      error && 'radio--error',
      className,
    );

    return (
      <label className={classes}>
        <input
          ref={ref}
          type="radio"
          className="radio__input"
          checked={checked}
          disabled={disabled}
          onChange={(e) => onChange?.(e.target.checked)}
          {...props}
        />
        <span className="radio__circle">
          <span className="radio__dot" />
        </span>
        {label && <span className="radio__label">{label}</span>}
      </label>
    );
  },
);

Radio.displayName = 'Radio';

Componentes relacionados

On this page

Detalles

Publicado14 de mayo de 2026
Categoriaforms
Lectura...
Visitas...
Ayuda?Slack

Componente

Radio

Source

components-react / cssDisponible via MCP: atom_uikit_source("radio")
Abrir en Storybook