Saltar al contenido
ssntl/ui

Checkbox

Un checkbox nativo donde toda la fila de la etiqueta es el objetivo y su texto nunca se selecciona.

Order alerts

Pick what reaches your phone.

Instalación

$ pnpm dlx shadcn@latest add @ssntl/checkbox

Uso

tsx
import { Checkbox, CheckboxLabel } from "@/components/ui/checkbox"

<div role="group" aria-labelledby="alertas-titulo">
  <p id="alertas-titulo" className="text-heading">Alertas de pedidos</p>
  <CheckboxLabel>
    <Checkbox name="alertas" value="pedidos" defaultChecked />
    Pedidos nuevos
  </CheckboxLabel>
</div>
  • Un input nativo: se envía con el formulario y funciona con teclado y lectores de pantalla.
  • La etiqueta envuelve la casilla, así toda la fila de 48px la activa; select-none evita que un doble clic seleccione el texto.
  • Agrupa las casillas bajo un rol de encabezado (text-heading), no texto de cuerpo, unido con aria-labelledby.
  • Los enlaces dentro de una etiqueta siguen funcionando; dales texto descriptivo.

Código

components/ui/checkbox.tsx
import * as React from "react"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

// A native checkbox, so forms, keyboard and screen readers work as they
// should. The box is 24px; CheckboxLabel makes the whole 48px row the target.
function Checkbox({
  className,
  ...props
}: Omit<React.ComponentProps<"input">, "type">) {
  return (
    <span
      data-slot="checkbox"
      className="inline-grid size-6 shrink-0 place-items-center"
    >
      <input
        type="checkbox"
        className={cn(
          "peer col-start-1 row-start-1 size-6 cursor-pointer appearance-none rounded-xs border border-input bg-card",
          "transition-[background-color,border-color] duration-(--duration-fast) ease-out checked:border-primary checked:bg-primary hover:border-foreground",
          "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
          "disabled:cursor-not-allowed aria-invalid:border-destructive",
          className
        )}
        {...props}
      />
      {/* A heavier stroke than the 1.6 default, so the mark reads at 16px. */}
      <Check
        aria-hidden
        strokeWidth={2}
        className="pointer-events-none col-start-1 row-start-1 size-4 scale-50 text-primary-foreground opacity-0 transition-[opacity,scale] duration-(--duration-fast) ease-out peer-checked:scale-100 peer-checked:opacity-100 motion-reduce:scale-100"
      />
    </span>
  )
}

// The label wraps the box, so the text toggles it too. select-none keeps a
// quick double click from highlighting the words instead of checking twice.
function CheckboxLabel({ className, ...props }: React.ComponentProps<"label">) {
  return (
    <label
      data-slot="checkbox-label"
      className={cn(
        "flex min-h-12 cursor-pointer items-center gap-3 text-body select-none has-disabled:cursor-not-allowed has-disabled:opacity-40",
        className
      )}
      {...props}
    />
  )
}

export { Checkbox, CheckboxLabel }