Button
Componente Button de shadcn - variantes, tamaños, estados de carga y botones con icono.
Busca en todas las páginas de la documentación
Componente Button de shadcn - variantes, tamaños, estados de carga y botones con icono.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
npx shadcn@latest add buttonimport { Button } from "@/components/ui/button";
// Variantes
<Button variant="default">Primario</Button>
<Button variant="secondary">Secundario</Button>
<Button variant="destructive">Eliminar</Button>
<Button variant="outline">Contorno</Button>
<Button variant="ghost">Discreto</Button>
<Button variant="link">Enlace</Button>
// Tamaños
<Button size="sm">Pequeño</Button>
<Button size="default">Predeterminado</Button>
<Button size="lg">Grande</Button>
<Button size="icon">Icono</Button>
// Como enlace
<Button asChild>
<a href="/about">Acerca de</a>
</Button>
// Deshabilitado
<Button disabled>Deshabilitado</Button>Cuándo usarlo: Para cada acción clicable en tu app - el componente Button ofrece estilos consistentes, accesibilidad y soporte de variantes.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Loader2, Plus, Trash2, Download, ExternalLink } from "lucide-react";
export function ButtonShowcase() {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
await new Promise((r) => setTimeout(r, 2000));
setLoading(false);
}
return (
<div className="space-y-6">
{/* Botón con carga */}
<div className="flex gap-3">
<Button onClick={handleClick} disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{loading ? "Guardando..." : "Guardar cambios"}
</Button>
</div>
{/* Botones con icono */}
<div className="flex gap-2">
<Button size="icon" variant="outline" aria-label="Añadir elemento">
<Plus className="h-4 w-4" />
</Button>
<Button size="icon" variant="destructive" aria-label="Eliminar elemento">
<Trash2 className="h-4 w-4" />
</Button>
</div>
{/* Botón con icono y texto */}
<div className="flex gap-3">
<Button>
<Download className="mr-2 h-4 w-4" />
Descargar
</Button>
<Button variant="outline" asChild>
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
Visitar sitio
<ExternalLink className="ml-2 h-4 w-4" />
</a>
</Button>
</div>
{/* Grupo de botones */}
<div className="inline-flex rounded-md shadow-sm">
<Button variant="outline" className="rounded-r-none border-r-0">Izquierda</Button>
<Button variant="outline" className="rounded-none border-r-0">Centro</Button>
<Button variant="outline" className="rounded-l-none">Derecha</Button>
</div>
{/* Ancho completo */}
<Button className="w-full" size="lg">
Botón de ancho completo
</Button>
</div>
);
}Lo que demuestra:
aria-label para accesibilidadasChild para renderizar como etiqueta anchorclass-variance-authority (cva) para definir combinaciones de variant y sizeasChild usa el componente Slot de Radix para fusionar props en el elemento hijocn() permite sobrescribir cualquier clase predeterminada mediante la prop className<button> nativo por defecto, heredando todos los atributos HTML de botónVariante personalizada mediante cva:
// components/ui/button.tsx - añade una variante personalizada
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
// ... otras variantes
success: "bg-green-600 text-white hover:bg-green-700",
warning: "bg-amber-500 text-white hover:bg-amber-600",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
xs: "h-7 rounded px-2 text-xs",
},
},
}
);Patrón de confirmar para eliminar:
function ConfirmDeleteButton({ onConfirm }: { onConfirm: () => void }) {
const [confirming, setConfirming] = useState(false);
if (confirming) {
return (
<div className="flex gap-2">
<Button variant="destructive" size="sm" onClick={onConfirm}>
Confirmar
</Button>
<Button variant="ghost" size="sm" onClick={() => setConfirming(false)}>
Cancelar
</Button>
</div>
);
}
return (
<Button variant="outline" size="sm" onClick={() => setConfirming(true)}>
<Trash2 className="mr-2 h-3 w-3" /> Eliminar
</Button>
);
}Botón de envío con useFormStatus:
"use client";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" disabled={pending}>
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{pending ? "Enviando..." : "Enviar"}
</Button>
);
}// Tipo ButtonProps
import { Button, type ButtonProps } from "@/components/ui/button";
// Extracción del tipo variant
import { type VariantProps } from "class-variance-authority";
type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
// "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
// Polimórfico con asChild
<Button asChild>
<Link href="/about">Acerca de</Link> {/* Next.js Link */}
</Button>asChild elimina el elemento button - El elemento hijo recibe todas las props del botón pero no está envuelto en un <button>. Solución: Asegúrate de que el hijo pueda aceptar onClick, className y otras props de botón.
Los botones con icono necesitan aria-label - Un botón solo con icono no tiene texto visible. Solución: Añade siempre aria-label para lectores de pantalla.
disabled impide todos los eventos - A diferencia de aria-disabled, el atributo nativo disabled elimina el botón del orden de tabulación. Solución: Usa aria-disabled si necesitas que el botón siga siendo enfocable mientras está inactivo.
Un botón dentro de un formulario envía por defecto - Los botones sin type usan type="submit" por defecto. Solución: Usa type="button" para botones que no envían dentro de formularios.
Sobrescribir variantes - className="bg-red-500" en un botón con variant="default" puede no ganar por especificidad. Solución: La utilidad cn() lo gestiona mediante tailwind-merge.
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
<button> nativo | Necesitas un botón puntual sin el sistema de variantes | Quieres estilos consistentes en toda la app |
| Radix Toggle | Necesitas un botón de alternancia con estado pressed | Un botón normal es suficiente |
<a> con estilo de botón | La acción navega a una URL | La acción dispara un evento del lado del cliente |
| Bibliotecas de botones con icono | Necesitas comportamiento especializado de botones con icono | shadcn Button con size="icon" lo cubre |
default - acciones principales (enviar, guardar)secondary - acciones menos prominentesdestructive - eliminar o acciones peligrosasoutline - acciones con borde y bajo énfasisghost - estilo mínimo, a menudo para barras de herramientaslink - con estilo de hipervínculoUsa la prop asChild para delegar el renderizado al elemento hijo:
<Button asChild>
<a href="/about">Acerca de</a>
</Button><Button onClick={handleClick} disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{loading ? "Guardando..." : "Guardar cambios"}
</Button>Slot de Radix para fusionar las props del Button en su único elemento hijo<button>; el hijo recibe onClick, className, etc.<a>, <Link> o cualquier elemento manteniendo los estilos de ButtonAñade la variante a la definición cva de buttonVariants en components/ui/button.tsx:
success: "bg-green-600 text-white hover:bg-green-700",
warning: "bg-amber-500 text-white hover:bg-amber-600",class-variance-authority (cva) define combinaciones de variant y sizecn() (impulsada por tailwind-merge) fusiona y deduplica clases de Tailwindtype explícito usan type="submit" por defectotype="button" para botones que no envían dentro de formularios<div className="inline-flex rounded-md shadow-sm">
<Button variant="outline" className="rounded-r-none border-r-0">Izquierda</Button>
<Button variant="outline" className="rounded-none border-r-0">Centro</Button>
<Button variant="outline" className="rounded-l-none">Derecha</Button>
</div>aria-label, la tecnología asistiva anuncia el botón sin un nombre significativoaria-label="Descripción" a los botones con size="icon"import { type VariantProps } from "class-variance-authority";
type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
// "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" disabled={pending}>
{pending ? "Enviando..." : "Enviar"}
</Button>
);
}className establecen la misma propiedadcn() usa tailwind-merge para resolverlo, así que debería funcionar - pero solo si pasas por cn()cn(), gana la última clase en la hoja de estilos, que puede no ser tu sobrescrituraRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥