ATOM
Components (registry)Forms

Toggle

A switch that slides between on and off without a form submit. Instant preferences and feature flags.

Preview

Editorial

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

## Ejemplos

Instant preference:

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

<Toggle label="Show activity status" checked={showStatus} onChange={setShowStatus} /> ```

## Accesibilidad

- Prefer the built-in `label` (or `aria-label`) — a naked switch has no accessible name. - `onChange` fires for immediate prefs; do not rely on a separate submit for the only state change.

### Correcto

- role='switch' — screen readers anuncian 'toggle' o 'switch', no 'checkbox' - Input nativo `<input type='checkbox'>` maneja el estado internamente - Touch target 44x40px cumple WCAG - Label clickeable — click en el texto togglea - focus-visible ring combinado con inset shadow (no los reemplaza) - Space togglea on/off (comportamiento nativo)

### Evitar

- No usar Toggle para seleccion multiple — usar Checkbox - No usar Toggle sin label a menos que el contexto sea obvio (ej: dentro de un settings row) - No simular toggle con divs — el input nativo ya maneja ARIA

## Cuándo no usar

- Form multi-select or “agree to terms” → `Checkbox`. - Choosing one of many plans → `Radio` group.

## Criterio de uso

- Usa Toggle para una preferencia que cambia de inmediato y que tiene sólo dos estados persistentes. - Comunica el efecto del cambio en la etiqueta, no sólo con “on/off”; el usuario debe entender qué queda activo. - Si la preferencia está bloqueada por plan o permisos, explica el motivo cercano al control en lugar de dejar un switch muerto.

## Gotchas

- Un switch sin label visible o `aria-label` no tiene nombre accesible. - No uses Toggle para enviar un formulario ni para seleccionar una opción dentro de un conjunto mutuamente excluyente. - **Nota**: La animacion de bounce es puro CSS (cubic-bezier(0.35, 1.5, 0.6, 1) — spring con overshoot). Se activa con la clase .toggle--animated. Sin ella, el thumb se mueve instantaneamente. - **Nota**: No requiere GSAP. El bounce es puro CSS cubic-bezier. La diferencia con easing-spring (0.34, 1.56, 0.64, 1) del sistema: este es mas lento y con menos damping, diseñado para el slide fisico del thumb.

Uso

import { Toggle } from '@/components/atoms/Toggle';

<Toggle checked={on} onChange={setOn} label="Email alerts" />

Props

PropTipoDefaultRango / opcionesWhatHow
checkedbooleanfalse`true` / `false`On/off state of the switch.Controlled: pass checked + onChange. Default uncontrolled depends on usage.
disabledbooleanfalse`true` / `false`Prevents toggling.true when the setting is locked by plan. Default: false.
animatedbooleanfalse`true` / `false`Thumb motion on toggle.true by default for preferences; false if reduced-motion is required at the call site. Default: false.

Gotchas

  • a11y

    Provide a visible label (label prop or Field) — a bare switch fails WCAG name.

Anatomía CSS

<div class="toggle">
  <span class="toggle__input"></span>
  <span class="toggle__label"></span>
  <span class="toggle__thumb"></span>
  <span class="toggle__track"></span>
</div>
ClasePropósito
toggleroot
toggle--animatedmodifier
toggle--disabledmodifier
toggle__inputelement
toggle__labelelement
toggle__thumbelement
toggle__trackelement

Tokens resueltos

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

VariantePropValorToken
allbg#0a0a0aforeground
allborder#0a0a0aforeground
allhover-bg#0a0a0aforeground
alldisabled-bg#e5e5e5border
allfg#0a0a0aforeground
alldisabled-fg#525252muted.foreground

Animaciones

PropiedadDuraciónEasing
background-colorvar(--duration-200)var(--easing-out)
border-colorvar(--duration-200)var(--easing-out)
box-shadowvar(--duration-150)var(--easing-out)
transformvar(--duration-500)cubic-bezier(0.35
1.5
0.6
1)

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).

/* -------------------------------------------------------------------------
   Toggle (Switch)

   Single size. Track + thumb that slides on checked.
   Native <input type="checkbox" role="switch"> hidden.

   Off:      muted track, white thumb with shadow
   On:       primary track, white thumb with shadow
   States:   unchecked, checked, disabled
   Figma:    Size=s (36x20), Size=m (44x24) — we use s (web standard)
   ------------------------------------------------------------------------- */

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

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

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

/* Track */
.toggle__track {
  position: relative;
  width: 2.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  border-radius: 9999px;
  border: 1px solid #e5e5e5;
  background-color: #f5f5f5;
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08);
  transition:
    background-color 200ms cubic-bezier(0.22, 1, 0.36, 1),
    border-color 200ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Thumb — absolute positioned for pixel-perfect alignment */
.toggle__thumb {
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 1rem;
  height: 1rem;
  border-radius: 9999px;
  background-color: #fafafa;
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  transform: translateX(0) rotate(0.001deg);
}

/* Animated toggle: smooth slide with bounce overshoot */
.toggle--animated .toggle__thumb {
  transition: transform 500ms cubic-bezier(0.35, 1.5, 0.6, 1);
}

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

.toggle__input:hover:not(:disabled) ~ .toggle__track {
  background-color: #e5e5e5;
}

.toggle__input:checked:hover:not(:disabled) ~ .toggle__track {
  background-color: #0a0a0a;
}

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

.toggle__input:checked ~ .toggle__track {
  background-color: #0a0a0a;
  border-color: #0a0a0a;
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.15);
}

.toggle__input:checked ~ .toggle__track .toggle__thumb {
  transform: translateX(calc(2.25rem - 0.25rem - 1rem));
}

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

.toggle__input:focus-visible ~ .toggle__track {
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 0 2px var(--focus-ring-color);
}

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

.toggle__input:disabled ~ .toggle__track {
  background-color: #f5f5f5;
}

.toggle__input:disabled:checked ~ .toggle__track {
  background-color: #f5f5f5;
}

.toggle__input:disabled ~ .toggle__track .toggle__thumb {
  background-color: #e5e5e5;
  box-shadow: none;
}

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

.toggle__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;
}

.toggle--disabled .toggle__label {
  color: #525252;
}

Codigo fuente

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

export type ToggleProps = {
  checked?: boolean;
  disabled?: boolean;
  animated?: 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 Toggle = forwardRef<HTMLInputElement, ToggleProps>(
  (
    {
      checked = false,
      disabled = false,
      animated = false,
      label,
      className,
      onChange,
      ...props
    },
    ref,
  ) => {
    const classes = cn(
      'toggle',
      disabled && 'toggle--disabled',
      animated && 'toggle--animated',
      className,
    );

    return (
      <label className={classes}>
        <input
          ref={ref}
          type="checkbox"
          role="switch"
          className="toggle__input"
          checked={checked}
          disabled={disabled}
          onChange={(e) => onChange?.(e.target.checked)}
          {...props}
        />
        <span className="toggle__track">
          <span className="toggle__thumb" />
        </span>
        {label && <span className="toggle__label">{label}</span>}
      </label>
    );
  },
);

Toggle.displayName = 'Toggle';

Componentes relacionados

On this page

Detalles

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

Componente

Toggle

Source

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