Mostrando entradas con la etiqueta radix ui. Mostrar todas las entradas
Mostrando entradas con la etiqueta radix ui. Mostrar todas las entradas

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? 

miércoles, 28 de enero de 2026

Radix UI vs Base UI (shadcn/ui)

 ¿Cómo es eso de que Shadcn/ui se puede utilizar con Radix UI pero también con Base UI?

Shadcn/ui ahora permite elegir entre dos librerías de componentes base: Radix UI o Base UI. Ambas ofrecen primitives accesibles y altamente personalizables, pero con diferencias en arquitectura y estilo de implementación. Podés usar cualquiera como base para tus componentes, según tus preferencias.

🔹 ¿Qué son Radix UI y Base UI?

CaracterísticaRadix UIBase UI
OrigenOriginal base de shadcn/uiAlternativa moderna creada por el mismo equipo
Estilo de APIUsa asChild con SlotUsa render prop con useRender
ModularidadPaquetes separados por componentePaquete único @base-ui/react
PopularidadMuy extendido en la comunidad ReactMás nuevo, pero con adopción creciente
MigraciónFácil gracias a documentación oficialCompatible con shadcn/ui desde diciembre 2025

🔹 ¿Cómo se elige entre Radix y Base UI?

Cuando ejecutás:

bash
npx shadcn-ui@latest init

El CLI te pregunta si querés usar Radix UI o Base UI como librería de componentes base. Esto define cómo se construyen los componentes como Dialog, Popover, Tabs, etc.

🔹 ¿Qué cambia entre una y otra?

Ejemplo con Radix UI (asChild):

tsx
<Button asChild>
  <a href="/contact">Contacto</a>
</Button>

Ejemplo con Base UI (render prop):

tsx
<Button render={() => <a href="/contact">Contacto</a>} />

👉 Ambos logran lo mismo, pero con estilos de API distintos.

🔹 ¿Puedo migrar de Radix a Base UI?

Sí. Shadcn/ui publicó una guía oficial para migrar componentes existentes de Radix a Base UI. La migración implica cambiar el uso de Slot por useRender, y ajustar props como asChild.

✨ ¿Cuál conviene usar?

  • Radix UI: si ya tenés experiencia o componentes basados en Radix.

  • Base UI: si preferís una API más moderna, con menos dependencias y un solo paquete.

  • Ambos son compatibles con shadcn/ui y tienen soporte completo.

¿Querés que te muestre cómo se ve el mismo Dialog en ambas versiones para comparar estilos y decidir cuál te conviene más?

¿Puedo saber si estoy usando Radix UI o Base UI mirando el package.json?

Exacto, Gabriel 👌. Podés saberlo mirando tu package.json porque cada opción instala dependencias distintas:

🔹 Si usás Radix UI

En tu package.json vas a ver dependencias como:

json
"dependencies": {
  "@radix-ui/react-dialog": "...",
  "@radix-ui/react-popover": "...",
  "@radix-ui/react-tabs": "...",
  // etc.
}

👉 Radix UI instala un paquete por cada primitive (react-dialog, react-popover, etc.).

🔹 Si usás Base UI

En cambio, si elegiste Base UI, vas a ver algo así:

json
"dependencies": {
  "@base-ui/react": "..."
}

👉 Base UI se distribuye como un solo paquete (@base-ui/react) que contiene todos los primitives.

✨ Conclusión

  • Si ves muchos paquetes con prefijo @radix-ui/react-* → estás usando Radix UI.

  • Si ves solo @base-ui/react → estás usando Base UI.

¿Querés que te muestre cómo se vería el mismo Dialog implementado con Radix y con Base UI, para que compares directamente las diferencias en código?

Sí.

Perfecto, Gabriel 🙌. Te muestro cómo se vería el mismo Dialog implementado con Radix UI y con Base UI, para que compares directamente las diferencias en código:

🔹 Con Radix UI

Radix usa el patrón asChild y Slot para permitir que un componente hijo se renderice dentro del trigger.

tsx
import * as Dialog from "@radix-ui/react-dialog";
import { Button } from "@/components/ui/button";

export function RadixDialogExample() {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <Button variant="outline">Abrir popup</Button>
      </Dialog.Trigger>
      <Dialog.Content className="p-6 bg-white rounded-lg shadow-lg">
        <Dialog.Title>Editar jabón</Dialog.Title>
        <Dialog.Description>
          Modificá los datos y guardalos en el back end.
        </Dialog.Description>
        <form className="space-y-4">
          <input type="text" placeholder="Nombre" className="border p-2 w-full" />
          <textarea placeholder="Descripción" className="border p-2 w-full" />
        </form>
        <Dialog.Close asChild>
          <Button variant="secondary">Cerrar</Button>
        </Dialog.Close>
      </Dialog.Content>
    </Dialog.Root>
  );
}

👉 Notá el uso de asChild en Dialog.Trigger y Dialog.Close.

🔹 Con Base UI

Base UI usa un enfoque distinto: el prop render para definir cómo se renderiza el trigger o el contenido.

tsx
import { Dialog } from "@base-ui/react";
import { Button } from "@/components/ui/button";

export function BaseUIDialogExample() {
  return (
    <Dialog>
      <Dialog.Trigger render={() => <Button variant="outline">Abrir popup</Button>} />
      <Dialog.Content className="p-6 bg-white rounded-lg shadow-lg">
        <Dialog.Title>Editar jabón</Dialog.Title>
        <Dialog.Description>
          Modificá los datos y guardalos en el back end.
        </Dialog.Description>
        <form className="space-y-4">
          <input type="text" placeholder="Nombre" className="border p-2 w-full" />
          <textarea placeholder="Descripción" className="border p-2 w-full" />
        </form>
        <Dialog.Close render={() => <Button variant="secondary">Cerrar</Button>} />
      </Dialog.Content>
    </Dialog>
  );
}

👉 Notá el uso de render={() => ...} en lugar de asChild.

✨ Diferencias clave

  • Radix UIasChild + Slot.

  • Base UIrender prop.

  • Ambos permiten inyectar cualquier JSX dentro del trigger o del contenido, pero con estilos de API distintos.

¿Querés que te arme un diagrama comparativo visual mostrando cómo fluye el trigger → content → close en Radix vs Base UI?

Sí.

Aquí tenés el diagrama comparativo visual entre Radix UI y Base UI aplicado al flujo de un Dialog ✨:

  • Radix UI

    • Usa Dialog.Trigger asChild → el botón se pasa como hijo.

    • Usa Dialog.Content → renderiza el contenido del popup.

    • Usa Dialog.Close asChild → el botón de cerrar se pasa como hijo.

    • El patrón clave es asChild + Slot.

  • Base UI

    • Usa Dialog.Trigger render={() => ...} → el botón se pasa mediante una función.

    • Usa Dialog.Content → igual que Radix, renderiza el contenido.

    • Usa Dialog.Close render={() => ...} → el botón de cerrar se pasa mediante función.

    • El patrón clave es render prop.

👉 Ambos siguen el mismo flujo: Trigger → Content → Close, pero con APIs distintas. Radix se apoya en composición con Slot, mientras que Base UI usa funciones de render.

¿Querés que te prepare un ejemplo extendido donde el mismo Dialog contenga un formulario con React Query y te muestre cómo cambia la implementación entre Radix y Base UI?