Saltar al contenido principal

shadcn/ui en Español: Guía Completa de Instalación y Uso 2026

Aprende a instalar y usar shadcn/ui en Next.js y React: qué es realmente, componentes, temas, personalización y por qué no es una librería tradicional.

Fran Cobos 5 min de lectura 869 palabras

Tabla de contenidos

shadcn/ui es lo más usado en el ecosistema React en 2026 para UI, pero genera confusión porque no funciona como cualquier otra librería. Esta guía explica qué es de verdad y cómo usarlo desde cero.

Qué es shadcn/ui (y qué no es)

No es una librería npm que instalas y usas como import { Button } from 'shadcn-ui'.

Es un CLI que copia componentes bien diseñados directamente a tu proyecto. El código queda en components/ui/ y es tuyo: lo puedes modificar sin restricciones.

Los componentes usan:

  • Radix UI para la lógica/accesibilidad (menús, diálogos, selects)
  • Tailwind CSS para los estilos
  • class-variance-authority (CVA) para variantes de componentes

Instalación en Next.js (App Router)

# Requisitos: Next.js + TypeScript + Tailwind CSS
npx create-next-app@latest mi-app --typescript --tailwind --app

cd mi-app

# Inicializar shadcn/ui
npx shadcn@latest init

El CLI preguntará:

Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Would you like to use CSS variables for colors? › yes

Esto crea:

  • components.json — configuración de shadcn
  • Actualiza tailwind.config.ts con las variables de color
  • Crea lib/utils.ts con la función cn()

Instalación en Vite + React

npm create vite@latest mi-app -- --template react-ts
cd mi-app
npm install
npm install -D tailwindcss @tailwindcss/vite

# Configurar Tailwind en vite.config.ts
# Luego inicializar shadcn
npx shadcn@latest init

Añadir componentes

# Componentes individuales
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add dialog
npx shadcn@latest add table
npx shadcn@latest add form

# Ver todos los disponibles
npx shadcn@latest add

Cada comando copia los archivos a components/ui/.


Usar los componentes

Button

import { Button } from '@/components/ui/button';

export function EjemploButton() {
  return (
    <div className="flex gap-4">
      <Button>Por defecto</Button>
      <Button variant="destructive">Eliminar</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button size="sm">Pequeño</Button>
      <Button size="lg">Grande</Button>
      <Button disabled>Deshabilitado</Button>
    </div>
  );
}

Input + Label + Form básico

import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';

export function FormLogin() {
  return (
    <form className="space-y-4 max-w-sm">
      <div className="space-y-2">
        <Label htmlFor="email">Email</Label>
        <Input
          id="email"
          type="email"
          placeholder="tu@ejemplo.com"
        />
      </div>
      <div className="space-y-2">
        <Label htmlFor="password">Contraseña</Label>
        <Input
          id="password"
          type="password"
          placeholder="••••••••"
        />
      </div>
      <Button type="submit" className="w-full">
        Iniciar sesión
      </Button>
    </form>
  );
}

Dialog (modal)

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';

export function ConfirmarEliminar({ onConfirmar }: { onConfirmar: () => void }) {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">Eliminar cuenta</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>¿Estás seguro?</DialogTitle>
          <DialogDescription>
            Esta acción no se puede deshacer. Se eliminará tu cuenta permanentemente.
          </DialogDescription>
        </DialogHeader>
        <div className="flex justify-end gap-3 mt-4">
          <Button variant="outline">Cancelar</Button>
          <Button variant="destructive" onClick={onConfirmar}>
            Sí, eliminar
          </Button>
        </div>
      </DialogContent>
    </Dialog>
  );
}

Tabla con datos

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

interface Usuario {
  id: string;
  nombre: string;
  email: string;
  plan: 'free' | 'pro';
}

export function TablaUsuarios({ usuarios }: { usuarios: Usuario[] }) {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Nombre</TableHead>
          <TableHead>Email</TableHead>
          <TableHead>Plan</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {usuarios.map((user) => (
          <TableRow key={user.id}>
            <TableCell className="font-medium">{user.nombre}</TableCell>
            <TableCell>{user.email}</TableCell>
            <TableCell>
              <span className={`px-2 py-1 rounded text-xs font-semibold ${
                user.plan === 'pro'
                  ? 'bg-blue-100 text-blue-800'
                  : 'bg-gray-100 text-gray-800'
              }`}>
                {user.plan.toUpperCase()}
              </span>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

Personalizar estilos

Cambiar colores del tema

/* app/globals.css */
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --primary: 221.2 83.2% 53.3%;       /* Azul personalizado */
    --primary-foreground: 210 40% 98%;
    --destructive: 0 84.2% 60.2%;       /* Rojo */
    --border: 214.3 31.8% 91.4%;
    --radius: 0.5rem;                    /* Bordes más o menos redondeados */
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --primary: 217.2 91.2% 59.8%;
    --border: 217.2 32.6% 17.5%;
  }
}

Modificar un componente directamente

Como el código está en tu repo, puedes editarlo:

// components/ui/button.tsx
// Añade una variante personalizada
const buttonVariants = cva(
  '...clases base...',
  {
    variants: {
      variant: {
        default: '...',
        destructive: '...',
        // ✅ Tu variante personalizada
        brand: 'bg-purple-600 text-white hover:bg-purple-700 shadow-lg',
      },
    },
  }
);

La función cn()

Todas las variantes de shadcn usan cn(), que combina clsx y tailwind-merge. Úsala en tus propios componentes:

// lib/utils.ts (creado por shadcn init)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
// Uso en tus componentes
import { cn } from '@/lib/utils';

function MiComponente({ className, activo }: { className?: string; activo: boolean }) {
  return (
    <div className={cn(
      'p-4 rounded-lg border',
      activo && 'border-blue-500 bg-blue-50',
      className // permite sobreescribir desde el padre
    )}>
      contenido
    </div>
  );
}

Cuándo no usar shadcn/ui

  • Si tu proyecto no usa Tailwind y no quieres introducirlo
  • Si necesitas SSR/SSG sin ningún JS en cliente (los componentes Radix requieren JS)
  • Si el diseño debe ser 100% personalizado sin base — en ese caso es más trabajo adaptar que construir desde cero

Para formularios complejos con shadcn, combina con React Hook Form y Zod. Para gestión de datos del servidor, TanStack Query v5 es el complemento natural si usas shadcn en tu UI.

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.