Button
Five variants, three sizes, isLoading state with inline spinner, auto-disabled during pending, full TypeScript props.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonVariant | primary | Visual style |
| size | 'sm' | 'md' | 'lg' | md | Padding and font size |
| isLoading | boolean | false | Shows spinner, disables button |
| loadingText | string | undefined | Text shown when isLoading is true |
| disabled | boolean | false | Disables button |
| fullWidth | boolean | false | width: 100% |
| children | ReactNode | required | Button content |
| ...props | ButtonHTMLAttributes | — | All standard button props |
Variants
| Variant | Use case |
|---|---|
| primary | Primary actions — form submit, main CTA |
| secondary | Secondary actions — cancel, alternative |
| ghost | Tertiary — subtle actions, icon buttons |
| destructive | Dangerous actions — delete, remove |
| outline | Bordered — 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