sábado, 28 de febrero de 2026

Glifo de Checkbox para estado=indeterminate

Quiero que el glyph para el estado indeterminate de un Checkbox de shadcn/ui con Radix UI sea una raya horizontal.


Para lograr que el checkbox de shadcn/ui (que utiliza Radix UI) muestre una raya horizontal en el estado indeterminate, debemos modificar el componente para que detecte dicho estado y renderice un icono diferente al de verificación (Check).

He preparado una versión completa y funcional del componente Checkbox junto con un ejemplo de uso para que puedas probarlo directamente.

Checkbox de Shadcn con Estado Indeterminado
Feb 28, 1:45 PM

import React, { useState, useEffect } from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check, Minus } from "lucide-react"
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

// Utilidad para manejar clases de Tailwind
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

/**
* Componente Checkbox modificado para soportar el glifo de raya horizontal
* en estado 'indeterminate'.
*/
const Checkbox = React.forwardRef(({ className, checked, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=indeterminate]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:text-primary-foreground",
className
)}
checked={checked}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
{checked === "indeterminate" ? (
<Minus className="h-3.5 w-3.5 stroke-[3]" />
) : (
<Check className="h-3.5 w-3.5 stroke-[3]" />
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

// --- Ejemplo de Implementación ---

const App = () => {
// El estado puede ser true, false o "indeterminate"
const [status, setStatus] = useState("indeterminate")

const toggleStatus = () => {
if (status === true) setStatus(false)
else if (status === false) setStatus("indeterminate")
else setStatus(true)
}

return (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-950 text-white p-6">
<div className="bg-slate-900 p-10 rounded-xl border border-slate-800 shadow-xl space-y-6 w-full max-w-md">
<h2 className="text-xl font-semibold text-center mb-4">Estado del Checkbox</h2>
<div className="flex items-center space-x-3 p-4 bg-slate-800/50 rounded-lg border border-slate-700">
<Checkbox
id="terms"
checked={status}
onCheckedChange={(val) => setStatus(val)}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
Aceptar términos y condiciones
</label>
</div>

<div className="grid grid-cols-1 gap-2">
<button
onClick={toggleStatus}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 rounded-md text-sm transition-colors"
>
Alternar estado (Ciclo: Indet {'->'} On {'->'} Off)
</button>
<div className="text-center mt-4">
<span className="text-xs text-slate-400 uppercase tracking-widest">Valor actual:</span>
<code className="block mt-1 text-blue-400 font-mono text-lg">
{JSON.stringify(status)}
</code>
</div>
</div>

<div className="pt-4 border-t border-slate-700">
<p className="text-xs text-slate-500 text-center">
Nota: Se usa el icono <strong>Minus</strong> de Lucide cuando el valor es <code className="text-slate-300">"indeterminate"</code>.
</p>
</div>
</div>
</div>
)
}

export default App


He realizado los siguientes cambios clave para que funcione:

  1. Icono Condicional: Dentro de CheckboxPrimitive.Indicator, he añadido una lógica que verifica si checked === "indeterminate". Si es así, renderiza el icono Minus (la raya horizontal); de lo contrario, renderiza el Check.

  2. Estilos de Datos: He añadido data-[state=indeterminate]:bg-primary en las clases de Tailwind del Root. Radix UI asigna automáticamente este atributo data-state cuando el prop checked es exactamente el string "indeterminate".

  3. Grosor del Trazo: He usado stroke-[3] en el componente Minus para que visualmente tenga un peso similar al icono de verificación por defecto de Radix.

¿Deseas que ajustemos también el comportamiento de los checkboxes "hijos" para que actualicen automáticamente al padre? 

No hay comentarios: