Button
Cuatro tamaños, tres variantes, y los estados que un botón real necesita — disabled, loading, ícono solo.
Playground
<Button variant="primary">
Acción
</Button>Todas las combinaciones
| xs | sm | md | lg | |
|---|---|---|---|---|
| primary | ||||
| outline | ||||
| ghost |
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./lib/cn";
import { ButtonGroupContext } from "./lib/button-group-context";
// exportado para estilizar un elemento que no puede ser <button> — un
// <Link> que se ve como botón, por ejemplo — sin duplicar las clases a mano
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 [&>svg]:shrink-0",
{
variants: {
variant: {
primary: "bg-primary text-primary-foreground hover:opacity-90",
outline: "border border-border bg-background hover:bg-muted",
ghost: "hover:bg-muted",
success: "bg-success text-success-foreground hover:opacity-90",
danger: "bg-danger text-danger-foreground hover:opacity-90",
warning: "bg-warning text-warning-foreground hover:opacity-90",
info: "bg-info text-info-foreground hover:opacity-90",
},
// el ícono escala con el tamaño del botón — un solo lugar decide
// cuánto mide, no una prop de tamaño de ícono aparte
size: {
xs: "h-7 px-2 text-xs [&>svg]:h-3.5 [&>svg]:w-3.5",
sm: "h-8 px-3 [&>svg]:h-4 [&>svg]:w-4",
md: "h-10 px-4 [&>svg]:h-4 [&>svg]:w-4",
lg: "h-12 px-6 text-base [&>svg]:h-5 [&>svg]:w-5",
},
// composes with size instead of duplicating its height as a fixed
// "icon" size — aspect-square copies whatever height xs/sm/md/lg
// already set, so there's one source of truth for each scale step
iconOnly: {
true: "aspect-square px-0",
false: "",
},
},
defaultVariants: {
variant: "primary",
size: "md",
iconOnly: false,
},
},
);
function Spinner({ className }: { className?: string }) {
return (
<svg className={cn("h-4 w-4 animate-spin", className)} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-90" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
);
}
export type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>;
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/** shows a spinner and disables the button — for an in-flight action */
loading?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, iconOnly, loading, disabled, children, ...props }, ref) => {
// dentro de un ButtonGroup, el size del grupo manda salvo que este
// botón puntual traiga el suyo propio — mismo criterio que AvatarGroup
const groupCtx = React.useContext(ButtonGroupContext);
const effectiveSize = size ?? groupCtx?.size;
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size: effectiveSize, iconOnly }), className)}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}
>
{loading && <Spinner />}
{children}
</button>
);
},
);
Button.displayName = "Button";Código real de packages/ui/src/button.tsx.
Con ícono
No hay prop icon — el ícono es un hijo más, antes o después del texto. El tamaño del ícono ya escala solo con size ([&>svg] en la clase base), así que no hay que ajustarlo a mano en cada botón.
<Button variant="danger">
<TrashIcon />
Eliminar
</Button>
<Button variant="outline">
Agregar
<PlusIcon />
</Button>Solo ícono
iconOnly se combina con cualquier size — no es un tamaño aparte, así que escala en los mismos xs / sm / md / lg en vez de quedar fijo en uno solo. Sin texto visible, así que aria-label deja de ser opcional.
<Button size="sm" iconOnly aria-label="Agregar">
<PlusIcon />
</Button>Estados
success / danger / warning / info son tokens compartidos, no algo que Button inventó — aquí Button los pinta como relleno sólido. Ver Colores para la paleta completa y cómo los lee cada componente.
<Button variant="danger">Eliminar</Button>
<Button variant="success">Confirmar</Button>
<Button variant="warning">Revisar</Button>
<Button variant="info">Ver detalles</Button>Uso
import { Button } from "./components/ui/button";
<Button variant="primary" size="md">
Acción
</Button>API
| Prop | Tipo | Default |
|---|---|---|
| variant | primary | outline | ghost | success | danger | warning | info | primary |
| size | xs | sm | md | lg | md |
| iconOnly | boolean | false |
| loading | boolean | false |
| disabled | boolean | false |
El resto son los atributos nativos de <button> — se pasan directo.