Button

Five variants, three sizes, isLoading state with inline spinner, auto-disabled during pending, full TypeScript props.

Props

PropTypeDefaultDescription
variantButtonVariantprimaryVisual style
size'sm' | 'md' | 'lg'mdPadding and font size
isLoadingbooleanfalseShows spinner, disables button
loadingTextstringundefinedText shown when isLoading is true
disabledbooleanfalseDisables button
fullWidthbooleanfalsewidth: 100%
childrenReactNoderequiredButton content
...propsButtonHTMLAttributesAll standard button props

Variants

VariantUse case
primaryPrimary actions — form submit, main CTA
secondarySecondary actions — cancel, alternative
ghostTertiary — subtle actions, icon buttons
destructiveDangerous actions — delete, remove
outlineBordered — neutral emphasis
<Button variant="primary">Save changes</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost">Learn more</Button>
<Button variant="destructive">Delete account</Button>
<Button variant="outline">View details</Button>

Loading state

When isLoading is true, the button:

  • Receives the disabled attribute — preventing clicks
  • Gets cursor-not-allowed and reduced opacity
  • Shows a small SVG spinner before the text (or loadingText if provided)
  • pointer-events: none — clicks are fully blocked
<Button
  isLoading={isPending}
  loadingText="Saving..."
>
  Save changes
</Button>

Usage

components/auth/LoginForm.tsx
const [isPending, startTransition] = useTransition()

function handleSubmit(formData: FormData) {
  startTransition(async () => {
    const result = await loginAction(formData)
    if (!result.success) {
      toast('error', result.error)
    }
  })
}

return (
  <form action={handleSubmit}>
    {/* inputs */}
    <Button
      type="submit"
      isLoading={isPending}
      loadingText="Signing in..."
      fullWidth
    >
      Sign in
    </Button>
  </form>
)

Accessibility

  • disabled attribute set when isLoading or disabled prop is true
  • focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] for keyboard navigation
  • aria-disabled is not needed — the native disabled attribute is used
  • If using Button as a link-like element, use an <a> tag or Next.js Link instead