ComponentsPrimitives
Button
Interactive button component for user actions
The Button component is the primary interactive element for triggering actions in your application. It supports multiple variants, sizes, and states.
Import
import { Button } from '@nim-ui/components';Playground
Edit the code — the preview re-renders as you type.
Editable
<div className="flex flex-wrap items-center gap-3"> <Button variant="primary">Approve batch</Button> <Button variant="outline">Export</Button> <Button variant="ghost" size="sm">Dismiss</Button> <Button loading>Saving</Button> </div>
Variants
Buttons accept six variant values, but default and primary render identically, so five distinct styles are shown below.
Code
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>Sizes
Four size options are available: small, medium (default), large, and extra large.
Code
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>States
Disabled
Code
<Button disabled>Disabled Button</Button>
<Button variant="outline" disabled>Disabled Outline</Button>Loading
Code
<Button loading>Loading...</Button>
<Button variant="outline" loading>Please wait</Button>Full Width
Code
<Button fullWidth>Full Width Button</Button>With Icons
Combine buttons with icons for enhanced visual communication.
Code
<Button variant="primary">
<ArrowRightIcon className="mr-2" />
Next
</Button>
<Button variant="outline">
<ArrowLeftIcon className="mr-2" />
Back
</Button>
<Button variant="ghost">
<RefreshIcon className="mr-2" />
Refresh
</Button>Props
| Name | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'default' | Visual style variant of the button |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Size of the button |
disabled | boolean | false | Natively disabled — removed from the tab order. Wins over loading; do not use it for the in-flight state |
loading | boolean | false | Show the spinner and suppress click activation, while the button stays focusable and aria-disabled |
loadingLabel | string | undefined | Opt-in screen-reader-only text next to the spinner. It joins the accessible name, so passing it renames the button while loading |
fullWidth | boolean | false | Whether the button should take full width of container |
type | 'button' | 'submit' | 'reset' | 'button' | HTML button type attribute |
onClick | (event: MouseEvent) => void | - | Click event handler |
className | string | - | Additional CSS classes to apply |
children* | ReactNode | - | Button content |
Usage Examples
Form Submit Button
function LoginForm() {
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await loginUser();
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<Button type="submit" loading={loading} fullWidth>
Sign In
</Button>
</form>
);
}Confirmation Dialog
function DeleteConfirmation({ onConfirm, onCancel }) {
return (
<div className="flex gap-3">
<Button variant="destructive" onClick={onConfirm}>
Delete
</Button>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
</div>
);
}Navigation
function Navigation() {
const navigate = useNavigate();
return (
<div className="flex gap-2">
<Button variant="ghost" onClick={() => navigate('/')}>
Home
</Button>
<Button variant="ghost" onClick={() => navigate('/about')}>
About
</Button>
<Button variant="primary" onClick={() => navigate('/signup')}>
Sign Up
</Button>
</div>
);
}Accessibility
The Button component follows WAI-ARIA best practices:
- Uses semantic
<button>element - Supports keyboard navigation (Enter, Space)
disabledis a nativedisabled;loadingisaria-disabledinstead, so the button keeps focus and its place in the tab order while a request is in flight (WCAG 2.2 SC 2.4.3 Focus Order)- Use
loading, notdisabled, for the in-flight state.disabledwins when both are set, which puts the button back out of the tab order and undoes the focus guarantee for exactly the users it exists for — sodisabled={isSubmitting || !isValid} loading={isSubmitting}is a bug. Writedisabled={!isValid} loading={isSubmitting} - Suppression while
loadingis scoped to click activation and the form submission that follows — pointer clicks, Enter and Space (a native<button>synthesises a click for both). Unlike nativedisabled, every other event still dispatches:pointerdown,mousedown,keydown, and capture-phase click listeners. A trigger that opens onpointerdown(RadixDropdownMenu,Popover,Select) will still open around a loading Button and needs its own guard - The loading state is carried by
aria-busyandaria-disabled.loadingLabelis opt-in because the text sits inside the button and joins its accessible name (<Button loading loadingLabel="Saving">Save</Button>→ "Saving Save"), which both renames the button and can make a screen reader re-announce it under the user's focus. (Can, not will — measured 2026-08-03, NVDA speaks a focused control's new name when it changes, VoiceOver stays quiet. Opt-in is the safe default precisely because the behaviour is not uniform.) ARIA treats a button's descendants as presentational, so therole="status"wrapper is markup parity withSpinner, not a live region — a guaranteed status announcement (SC 4.1.3) needs a live region outside the button, whichButtoncannot own without wrapping itself. That prune was a reading of the spec until it was measured: on NVDA 2026.1.1 with Firefox, 2026-08-03, a loading Button announcesLoading..., button, unavailable, busyand nothing at all comes from the wrapper. The same run reached it by Tab, which is thearia-disabledguarantee above holding in practice. Confirmed on VoiceOver with Safari (macOS 26.5.2), 2026-08-03, which announcesLoading... website button busy dimmed button— again nothing from the wrapper, and again reached by Tab. JAWS has not been run - Focus visible states for keyboard users
Keyboard Support
| Key | Action |
|---|---|
| Enter | Activates the button |
| Space | Activates the button |
| Tab | Moves focus to next focusable element |
| Shift + Tab | Moves focus to previous focusable element |
Best Practices
Do
- Use
primaryvariant for main actions - Use
destructivevariant for destructive actions - Add loading states for async operations
- Include descriptive labels
- Use icons to enhance clarity
Don't
- Use multiple primary buttons in the same context
- Make buttons too small for touch targets (minimum 44x44px)
- Use buttons for navigation (use links instead)
- Rely on color alone to convey meaning
- Create overly long button labels
Customization
Custom Colors
<Button className="bg-purple-600 hover:bg-purple-700 text-white">
Custom Color
</Button>Custom Rounded Corners
<Button className="rounded-full">
Rounded Button
</Button>Custom Shadow
<Button className="shadow-lg hover:shadow-xl">
Elevated Button
</Button>