Botones
Variantes, tamanos y estilos de botones
Botones
El componente Button incluye multiples variantes y tamanos. Usa class-variance-authority para las variantes y @radix-ui/react-slot para composicion con asChild.
Variantes
import { Button } from "@/components/ui/button";
// Default - accion principal
<Button>Guardar cambios</Button>
// Secondary - accion secundaria
<Button variant="secondary">Cancelar</Button>
// Destructive - acciones peligrosas
<Button variant="destructive">Eliminar</Button>
// Outline - estilo con borde
<Button variant="outline">Ver más</Button>
// Ghost - solo texto
<Button variant="ghost">Cerrar</Button>
// Link - como enlace
<Button variant="link">Leer más</Button>Tamanos
El boilerplate incluye un tamano xl adicional que no viene por defecto en shadcn/ui. Se usa en los CTAs del Hero y la landing.
<Button size="sm">Pequeño</Button> {/* h-8 */}
<Button size="default">Normal</Button> {/* h-9 */}
<Button size="lg">Grande</Button> {/* h-10 */}
<Button size="xl">Extra grande</Button> {/* h-12, text-base */}
<Button size="icon"> {/* size-9 */}
<Plus className="w-4 h-4" />
</Button>Definicion de tamanos en el boilerplate:
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
xl: "h-12 rounded-md px-8 text-base has-[>svg]:px-6",
icon: "size-9",
},Con iconos
import { Loader2, Plus, ArrowRight } from "lucide-react";
// Icono a la izquierda
<Button>
<Plus className="w-4 h-4 mr-2" />
Agregar
</Button>
// Icono a la derecha
<Button>
Continuar
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
// Estado de carga
<Button disabled>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Guardando...
</Button>Como enlace
Usa asChild para renderizar el boton como un Link de Next.js:
import Link from "next/link";
<Button asChild>
<Link href="/dashboard">Ir al dashboard</Link>
</Button>
// Con tamano xl como en el Hero
<Button size="xl" asChild>
<Link href="/#pricing">
Empieza ahora <ArrowRight className="ml-1 h-5 w-5" />
</Link>
</Button>CheckoutButton
El boilerplate incluye un componente CheckoutButton que maneja el flujo de checkout con Stripe:
"use client";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
interface CheckoutButtonProps {
priceId: string;
planName: string;
children: React.ReactNode;
className?: string;
variant?: "default" | "outline" | "secondary" | "ghost" | "link" | "destructive";
}
export function CheckoutButton({
priceId,
planName,
children,
className,
variant = "default",
}: CheckoutButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const handleCheckout = async () => {
setIsLoading(true);
try {
const response = await fetch("/api/stripe/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId, planName }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Error al crear el checkout");
}
const { url } = await response.json();
if (url) {
window.location.href = url;
}
} catch (error) {
console.error("Checkout error:", error);
toast.error(
error instanceof Error
? error.message
: "Error al procesar el pago. Inténtalo de nuevo."
);
} finally {
setIsLoading(false);
}
};
return (
<Button
variant={variant}
className={cn("w-full", className)}
onClick={handleCheckout}
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Procesando...
</>
) : (
children
)}
</Button>
);
}Uso en la sección de Pricing:
<CheckoutButton
priceId={plan.priceId}
planName={plan.name}
variant={plan.isPopular ? "default" : "outline"}
>
{plan.cta}
</CheckoutButton>Definicion completa del componente
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
xl: "h-12 rounded-md px-8 text-base has-[>svg]:px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }