Overview

The base Button (components/ui/Button.tsx) wraps shadcn/ui patterns with CVA for variants/sizes, Radix Slot for asChild, and our theme tokens for color, focus rings, and dark mode. Use this Button instead of raw <button> to keep styling and accessibility consistent across the app.

Variants & sizes

  • variant (visual style): default, highlight, outline, destructive, ghost, link, icon size (spacing/shape): sm, md, lg, icon
  • Defaults: variant="default", size="default". Use className only for small local tweaks; add/adjust CVA variants when patterns repeat.
import { Button } from '@/components/ui/Button';

export function Examples() {
  return (
    <div className="flex flex-wrap gap-3">
      <Button>Default</Button>
      <Button variant="highlight">Highlight</Button>
      <Button variant="outline" size="lg">Outline</Button>
      <Button variant="destructive">Delete</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link" asChild>
        <a href="/terms">Link</a>
      </Button>
      <Button variant="icon" size="icon" aria-label="Like">
        {/* your icon here */}
        <svg width="16" height="16" aria-hidden="true" />
      </Button>
    </div>
  );
}

Composition

Render a different element (e.g., Next.js Link) while keeping Button styles and focus rings:

import Link from 'next/link';
import { Button } from '@/components/ui/Button';

export function AsChildLink() {
  return (
    <Button asChild>
      <Link href="/pricing">View pricing</Link>
    </Button>
  );
}

Avoid nesting interactive elements (e.g., a <button> inside a <Link>). asChild solves this cleanly.

Focus & accessibility

The base button ships with tokenized focus rings and proper disabled behavior. Guidance:

  • Use type="button" for non-form buttons to avoid accidental submits.
  • Icon-only buttons must be labeled (use aria-label or visually hidden text).
  • Prefer disabled over intercepting clicks; the component already dims and removes pointer events.

Loading & async states

Click-driven async (client): Use TransitionButton with useTransition() so the UI stays responsive and isPending drives the spinner/text.

'use client';

import { TransitionButton } from '@/components/TransitionButton';

export function AsyncClick() {
  const [isPending, startTransition] = React.useTransition();

  async function handle() {
    startTransition(async () => {
      // await your async work...
      // e.g., await someServerAction();
    });
  }

  return (
    <TransitionButton
      isPending={isPending}
      pendingText="Working…"
      onClick={handle}
    >
      Run task
    </TransitionButton>
  );
}

Form submit (server action): Use SubmitButton. It reads useFormStatus() to switch to a pending state while the closest <form> action runs—no manual state or router calls needed.

'use client';

import { SubmitButton } from '@/components/SubmitButton';

export function FormExample() {
  async function action(formData: FormData) {
    'use server';
    // handle server action
  }

  return (
    <form action={action}>
      {/* fields... */}
      <SubmitButton pendingText="Submitting…">Submit</SubmitButton>
    </form>
  );
}

Specialized buttons

  • CheckoutButton / BuyButtonTemplate / SubscriptionButtonTemplate: high-level Lemon Squeezy flows; use TransitionButton, show toasts on failure, redirect on success.
  • WaitlistButtonTemplate: opens JoinWaitlistDialog for consistent CTA flow.
  • SocialLoginButton: posts the chosen OAuth provider; uses variant="icon" / size="icon" with provider icons.
  • SignOutButton: uses useTransition to call signOutAction, shows a toast, refreshes state, and redirects.

When to choose what

  • Normal clickable control → Button
  • Link styled as a button → Button asChild + Link
  • Spinner + pending text for async click → TransitionButton
  • Submitting a form (Server Actions) → SubmitButton
  • OAuth icon button → SocialLoginButton (variant="icon" / size="icon")

Extending styles

If a new treatment repeats, add a variant/size in the CVA map (components/ui/Button.tsx). Keep names semantic (highlight, destructive, …). Use tokens and Tailwind utilities; avoid raw hex values.