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-labelfor icon-only buttons - Use
loadingprop for async operations (shows spinner automatically) - Use
state="success"orstate="error"for post-action feedback - Use
disabledprop 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}andstate="loading"together (useloadingprop instead) - Don’t use both
loading={true}andstate="loading"together (useloadingprop instead)
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "subtle" | "default" | No | Visual variant of the button |
size | "sm" | "default" | "lg" | "xl" | "icon" | "default" | No | Size of the button |
state | "default" | "loading" | "success" | "error" | "default" | No | Visual 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. |
loading | boolean | false | No | Whether the button is in a loading state. When true, automatically sets state to “loading” and shows a spinner. |
loadingText | string | undefined | No | Text to display when loading is true. If not provided, the button’s children will be shown. |
disabled | boolean | false | No | Whether the button is disabled |
type | "button" | "submit" | "reset" | "button" | No | HTML button type attribute |
onClick | (event: React.MouseEvent<HTMLButtonElement>) => void | undefined | No | Click event handler |
asChild | boolean | false | No | Render as child element (for composition) |
className | string | undefined | No | Additional CSS classes |
children | React.ReactNode | undefined | No | Button content |
Standard HTML Button Props
The Button component extends all standard HTML button attributes, including:
id,name,value,form,formAction,formEncType,formMethod,formNoValidate,formTargetaria-*attributes for accessibilitydata-*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 meaninglessdisabledattribute. A slotted<button>still gets the real attribute, because it understands it.- The button looks disabled. The styling is keyed on
aria-disabledas well as:disabled, since:disablednever 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