ATOM
Components (registry)Forms

Checkbox

Ticks on or off; supports indeterminate when some children are selected. Multi-select lists and filters.

Preview

Editorial

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

## Ejemplos

Multi-select with label:

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

<Checkbox label="Email me product updates" checked={optIn} onChange={setOptIn} /> ```

Parent row with partial selection:

```tsx <Checkbox label="Select all" checked={all ? true : some ? 'indeterminate' : false} onChange={toggleAll} /> ```

## Accesibilidad

- Prefer the built-in `label` prop (associates the control). If you omit it, supply an external label via `id`/`htmlFor` or `aria-label`. - `checked="indeterminate"` is for “some children selected” — still expose the result in surrounding UI text when it matters.

### Correcto

- Input nativo `<input type='checkbox'>` maneja el estado — screen readers lo leen correctamente - Touch target de 40x40px (input invisible expandido) cumple WCAG minimum 44px aprox - Label envuelve todo el componente — click en el texto tambien togglea - focus-visible muestra ring solo por teclado, no por click - data-indeterminate + ref.indeterminate para el estado parcial - Disabled remueve del tab order via atributo disabled nativo

### Evitar

- No usar Checkbox sin label en contextos donde no hay otro indicador visual - No simular checkbox con divs — el input nativo ya maneja ARIA internamente - No usar error sin explicar el motivo (combinar con Field o texto de ayuda)

## Cuándo no usar

- Exactly one of many options → `Radio` group. - Instant on/off preference with a switch affordance → `Toggle`.

## Criterio de uso

- Usa checkbox para selecciones independientes o para aceptar una condición explícita; varias opciones pueden quedar activas al mismo tiempo. - El estado indeterminate representa una relación parcial, como “algunos hijos seleccionados”; no lo trates como un tercer valor persistente. - En filtros, aplica la selección sin perder contexto y comunica cuántos resultados cambian cuando el efecto no es obvio.

## Gotchas

- El estado indeterminate es visual: mantén el estado accesible y el texto circundante sincronizados. - No uses checkbox para una decisión mutuamente excluyente; en ese caso el usuario necesita `Radio`. - **Ojo**: Para indeterminate en vanilla, necesitas JS: document.querySelector('.checkbox__input').indeterminate = true. El atributo data-indeterminate activa los estilos CSS pero no el estado nativo.

Uso

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

<Checkbox checked={ok} onChange={setOk} label="I agree" />

Props

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

Gotchas

  • a11y

    Indeterminate is visual only — keep the accessible checked state in sync for AT.

Anatomía CSS

<div class="checkbox">
  <span class="checkbox__box"></span>
  <span class="checkbox__icon"></span>
  <span class="checkbox__input"></span>
  <span class="checkbox__label"></span>
</div>
ClasePropósito
checkboxroot
checkbox--disabledmodifier
checkbox--errormodifier
checkbox__boxelement
checkbox__iconelement
checkbox__inputelement
checkbox__labelelement

Tokens resueltos

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

VariantePropValorToken
uncheckedbg#fafafacheckbox.bg.unchecked
unchecked-hoverbg#f5f5f5checkbox.bg.unchecked.hover
checkedbg#0a0a0acheckbox.bg.checked
checkedfg#fafafacheckbox.fg.checked
checked-hoverbg#262626checkbox.bg.checked.hover
disabledbg#f5f5f5checkbox.bg.disabled
disabledfg#525252checkbox.fg.disabled
disabledborder#f5f5f5checkbox.border.disabled
allborder.default#525252checkbox.border.default

Animaciones

PropiedadDuraciónEasing
background-colorvar(--duration-150)var(--easing-out)
border-colorvar(--duration-150)var(--easing-out)
box-shadowvar(--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).

/* -------------------------------------------------------------------------
   Checkbox

   Single size (16px box, 40px touch target).
   Native <input type="checkbox"> hidden, visual replacement via label.

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

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

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

/* Hidden native input with touch target */
.checkbox__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 box */
.checkbox__box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  border-radius: 4px;
  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);
}

/* Check / minus icon */
.checkbox__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 0.75rem;
  height: 0.75rem;
  color: #fafafa;
}

.checkbox__icon svg {
  width: 100%;
  height: 100%;
}

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

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

.checkbox__input:checked:hover:not(:disabled) ~ .checkbox__box,
.checkbox__input[data-indeterminate]:hover:not(:disabled) ~ .checkbox__box {
  background-color: #262626;
}

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

.checkbox__input:checked ~ .checkbox__box,
.checkbox__input[data-indeterminate] ~ .checkbox__box {
  background-color: #0a0a0a;
  border-color: #0a0a0a;
}


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

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

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

.checkbox__input:disabled ~ .checkbox__box {
  background-color: #f5f5f5;
  border-color: #f5f5f5;
}

.checkbox__input:disabled ~ .checkbox__box .checkbox__icon {
  color: #525252;
}

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

.checkbox--error .checkbox__box {
  border-color: #f84131;
}

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

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

.checkbox--disabled .checkbox__label {
  color: #525252;
}

Codigo fuente

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

export type CheckboxProps = {
  checked?: boolean | 'indeterminate';
  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(' ');
}

const CheckIcon = () => (
  <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M2.5 6L5 8.5L9.5 3.5" />
  </svg>
);

const MinusIcon = () => (
  <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
    <path d="M2.5 6H9.5" />
  </svg>
);

export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
  (
    {
      checked = false,
      disabled = false,
      error = false,
      label,
      className,
      onChange,
      ...props
    },
    forwardedRef,
  ) => {
    const innerRef = useRef<HTMLInputElement>(null);
    const ref = (forwardedRef as React.RefObject<HTMLInputElement>) || innerRef;
    const isIndeterminate = checked === 'indeterminate';
    const isChecked = checked === true;

    useEffect(() => {
      if (ref && 'current' in ref && ref.current) {
        ref.current.indeterminate = isIndeterminate;
      }
    }, [isIndeterminate, ref]);

    const classes = cn(
      'checkbox',
      disabled && 'checkbox--disabled',
      error && 'checkbox--error',
      className,
    );

    return (
      <label className={classes}>
        <input
          ref={ref}
          type="checkbox"
          className="checkbox__input"
          checked={isChecked || isIndeterminate}
          disabled={disabled}
          onChange={(e) => onChange?.(e.target.checked)}
          {...(isIndeterminate ? { 'data-indeterminate': '' } : {})}
          {...props}
        />
        <span className="checkbox__box">
          {isChecked && (
            <span className="checkbox__icon">
              <CheckIcon />
            </span>
          )}
          {isIndeterminate && (
            <span className="checkbox__icon">
              <MinusIcon />
            </span>
          )}
        </span>
        {label && <span className="checkbox__label">{label}</span>}
      </label>
    );
  },
);

Checkbox.displayName = 'Checkbox';

Componentes relacionados

On this page

Detalles

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

Componente

Checkbox

Source

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