EmpiezaTuSaaS

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:

components/ui/button.tsx
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:

components/checkout-button.tsx
"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

components/ui/button.tsx
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 }

On this page