Skip to Content

Button

The Button component provides you with different button variants and sizes.

Live Examples

Live Example
☀️
Copy
View Code
import { Button } from '@jarllyng/nostromo' export default function ButtonDefault() { return <Button>Click me</Button> }...
Live Example
☀️
Copy
View Code
import { Button } from '@jarllyng/nostromo' export default function ButtonExamples() { return ( <div className="flex flex-wrap gap-4"> <Button variant="default">Default</Button> <Button variant="secondary">Secondary</Button> <Button variant="destructive">Destructive</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button>...

Import

import { Button } from "@jarllyng/nostromo"; // or import { Button } from "@jarllyng/nostromo/button";

Usage

import { Button } from "@jarllyng/nostromo"; export default function Example() { return ( <Button variant="default" size="default"> Click me </Button> ); }

Sizes

Live Example
☀️
Copy
View Code
import { Button } from '@jarllyng/nostromo' export default function ButtonSizes() { return ( <div className="space-x-4"> <Button size="sm">Small</Button> <Button size="default">Default</Button> <Button size="lg">Large</Button> </div> )...

Accessibility

Keyboard Navigation

  • Enter/Space: Activates the button
  • Tab: Moves focus to the button
  • Escape: Closes any associated dialogs or menus

ARIA Attributes

  • role=“button”: Identifies the element as a button
  • aria-disabled: Indicates when the button is disabled
  • aria-pressed: For toggle buttons (when applicable)

Focus Management

  • Visible focus indicator: Clear visual focus state
  • Focus trap: Prevents focus from escaping modal contexts
  • Focus restoration: Returns focus to trigger element

Screen Reader Support

  • Semantic HTML: Uses proper <button> element
  • Descriptive text: Clear button labels and descriptions
  • State announcements: Disabled and loading states are announced

Examples

// Accessible button with proper labeling <Button aria-label="Close dialog"> <XIcon /> </Button> // Button with loading state (recommended approach) <Button loading={isLoading} loadingText="Saving..." aria-describedby="loading-text"> Save </Button> <div id="loading-text" className="sr-only"> Please wait while your changes are saved </div> // Button with success/error feedback after action <Button state={saveResult === 'success' ? 'success' : saveResult === 'error' ? 'error' : 'default'}> {saveResult === 'success' ? 'Saved!' : saveResult === 'error' ? 'Failed' : 'Save'} </Button> // Button in form - use type="button" to prevent form submission <Button type="button" onClick={handleCancel}> Cancel </Button> <Button type="submit"> Submit </Button>

Best Practices

Do ✅

  • Use type="button" for buttons that don’t submit forms
  • Use type="submit" for form submission buttons
  • Provide aria-label for icon-only buttons
  • Use loading prop for async operations (shows spinner automatically)
  • Use state="success" or state="error" for post-action feedback
  • Use disabled prop to prevent interactions

Don’t ❌

  • Don’t use buttons for navigation (use links instead)
  • Don’t disable buttons without providing feedback
  • Don’t forget to handle loading states in forms
  • Don’t use buttons without accessible labels
  • Don’t use both loading={true} and state="loading" together (use loading prop instead)
  • Don’t use both loading={true} and state="loading" together (use loading prop instead)

Props

PropTypeDefaultRequiredDescription
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "subtle""default"NoVisual variant of the button
size"sm" | "default" | "lg" | "xl" | "icon""default"NoSize of the button
state"default" | "loading" | "success" | "error""default"NoVisual state of the button. Note: If loading is true, this prop is ignored and state becomes “loading”. Use state="success" or state="error" for post-action feedback.
loadingbooleanfalseNoWhether the button is in a loading state. When true, automatically sets state to “loading” and shows a spinner.
loadingTextstringundefinedNoText to display when loading is true. If not provided, the button’s children will be shown.
disabledbooleanfalseNoWhether the button is disabled
type"button" | "submit" | "reset""button"NoHTML button type attribute
onClick(event: React.MouseEvent<HTMLButtonElement>) => voidundefinedNoClick event handler
asChildbooleanfalseNoRender as child element (for composition)
classNamestringundefinedNoAdditional CSS classes
childrenReact.ReactNodeundefinedNoButton content

Standard HTML Button Props

The Button component extends all standard HTML button attributes, including:

  • id, name, value, form, formAction, formEncType, formMethod, formNoValidate, formTarget
  • aria-* attributes for accessibility
  • data-* attributes for custom data
  • All other standard HTML button attributes

asChild, disabled and loading

asChild renders whatever element you pass instead of a <button>, usually a link:

<Button asChild> <a href="/checkout">Continue</a> </Button>

disabled and loading work on that link too, and blocking it takes more than an attribute. disabled is a form-control attribute: put it on an anchor and the browser ignores it, so the link would announce itself disabled and then navigate anyway.

What happens instead:

  • The click and the Enter or Space key are both cancelled, in the capture phase, so neither your handler nor the child’s own handler runs and the navigation does not happen.
  • aria-disabled="true" is set, and no meaningless disabled attribute. A slotted <button> still gets the real attribute, because it understands it.
  • The button looks disabled. The styling is keyed on aria-disabled as well as :disabled, since :disabled never matches an anchor.
  • It stays focusable. That is the aria-disabled convention: a disabled control that has been removed from the tab order cannot be found by someone navigating by keyboard, so they never learn it is there.

One limit worth knowing: a capture handler on the child element itself runs before the guard and is not blocked. Regular onClick on either side is.

<Button asChild disabled onClick={parentHandler}> <a href="/checkout" onClick={childHandler}> Continue </a> </Button> // Click: neither handler runs, and the page does not navigate.
Last updated on