NimUI
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.

Button Variants
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.

Button Sizes
Code
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>

States

Disabled

Disabled State
Code
<Button disabled>Disabled Button</Button>
<Button variant="outline" disabled>Disabled Outline</Button>

Loading

Loading State
Code
<Button loading>Loading...</Button>
<Button variant="outline" loading>Please wait</Button>

Full Width

Full Width Button
Code
<Button fullWidth>Full Width Button</Button>

With Icons

Combine buttons with icons for enhanced visual communication.

Buttons with Icons
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

NameTypeDefaultDescription
variant'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive''default'Visual style variant of the button
size'sm' | 'md' | 'lg' | 'xl''md'Size of the button
disabledbooleanfalseNatively disabled — removed from the tab order. Wins over loading; do not use it for the in-flight state
loadingbooleanfalseShow the spinner and suppress click activation, while the button stays focusable and aria-disabled
loadingLabelstringundefinedOpt-in screen-reader-only text next to the spinner. It joins the accessible name, so passing it renames the button while loading
fullWidthbooleanfalseWhether the button should take full width of container
type'button' | 'submit' | 'reset''button'HTML button type attribute
onClick(event: MouseEvent) => void-Click event handler
classNamestring-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>
  );
}
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)
  • disabled is a native disabled; loading is aria-disabled instead, 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, not disabled, for the in-flight state. disabled wins 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 — so disabled={isSubmitting || !isValid} loading={isSubmitting} is a bug. Write disabled={!isValid} loading={isSubmitting}
  • Suppression while loading is scoped to click activation and the form submission that follows — pointer clicks, Enter and Space (a native <button> synthesises a click for both). Unlike native disabled, every other event still dispatches: pointerdown, mousedown, keydown, and capture-phase click listeners. A trigger that opens on pointerdown (Radix DropdownMenu, Popover, Select) will still open around a loading Button and needs its own guard
  • The loading state is carried by aria-busy and aria-disabled. loadingLabel is 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 the role="status" wrapper is markup parity with Spinner, not a live region — a guaranteed status announcement (SC 4.1.3) needs a live region outside the button, which Button cannot 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 announces Loading..., button, unavailable, busy and nothing at all comes from the wrapper. The same run reached it by Tab, which is the aria-disabled guarantee above holding in practice. Confirmed on VoiceOver with Safari (macOS 26.5.2), 2026-08-03, which announces Loading... 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

KeyAction
EnterActivates the button
SpaceActivates the button
TabMoves focus to next focusable element
Shift + TabMoves focus to previous focusable element

Best Practices

Do

  • Use primary variant for main actions
  • Use destructive variant 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>

On this page