Components

Button

A clickable element which communicates that users can trigger an action

When to use

✓

Use for actions, not navigation

Submitting forms, closing modals, triggering operations, confirming or cancelling. Anything that changes state in the app.

✓

Limit to one primary action per visible context

Only one variant="primary" should be visible at a time on a screen — that's the main thing the user is here to do.

✗

Don't use for navigation between pages

Use a <Link>, or <Button asChild><Link/></Button> for a button-styled link. Navigation buttons confuse SEO, screen readers, and browser back-stack behaviour.

✗

Don't use for toggling settings

Use Switch for on/off, Checkbox for opt-in, RadioGroup for mutually exclusive choice.

✗

Don't use for icon-only triggers

Use IconButton instead — it enforces an aria-label for accessibility.

Installation

npm install @marmoui/ui
yarn add @marmoui/ui
pnpm add @marmoui/ui
bun add @marmoui/ui

Usage

Import the Button component from the package. For examples with icons, you'll also need to import from react-icons/md:

import { Button } from '@marmoui/ui';
import { MdAdd, MdSave, MdEdit, MdArrowForward, MdDownload, MdOpenInNew, MdDelete } from 'react-icons/md';

Examples

Basic Button

Default Button

The simplest form of a button

<Button>Click me</Button>

Variants

Buttons come in different visual styles to indicate their importance and purpose.

Button Variants

Different visual styles for different use cases

<div className="flex flex-wrap gap-2">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="ghost-body">Ghost Body</Button>
<Button variant="ghost-destructive">Ghost Destructive</Button>
<Button variant="link">Link</Button>
</div>

Sizes

Buttons are available in four sizes to fit different contexts and density needs.

Button Sizes

Four size options from extra small to large

<div className="flex items-center gap-2">
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>

Size recommendations:

  • xs: Compact spaces, tables, crowded interfaces
  • sm: Default density interfaces, secondary actions
  • md: Default size for most primary actions
  • lg: Prominent calls-to-action, hero sections

With Icons

Buttons support icons on both the left and right sides, or as standalone icon-only buttons.

Using leftIcon and rightIcon props:

Buttons with Left Icons

Use the leftIcon prop for icons before text

<div className="flex flex-wrap gap-2">
<Button variant="primary" leftIcon={<MdAdd />}>
  Add Item
</Button>
<Button leftIcon={<MdSave />}>
  Save Changes
</Button>
<Button variant="secondary" leftIcon={<MdEdit />}>
  Edit
</Button>
</div>

Right side icons:

Buttons with Right Icons

Use the rightIcon prop for icons after text

<div className="flex flex-wrap gap-2">
<Button variant="primary" rightIcon={<MdArrowForward />}>
  Continue
</Button>
<Button rightIcon={<MdDownload />}>
  Download
</Button>
<Button variant="link" rightIcon={<MdOpenInNew />}>
  Open Link
</Button>
</div>

Loading State

Buttons can display a loading state to provide feedback during async operations.

Loading Buttons

Show loading state during async operations

<div className="flex flex-wrap gap-2">
<Button variant="primary" loading disabled>
  Processing...
</Button>
<Button variant="primary" loading loadingText="Saving..." disabled>
  Save Changes
</Button>
<Button variant="secondary" loading disabled>
  Loading
</Button>
<Button variant="destructive" loading loadingText="Deleting..." disabled>
  Delete
</Button>
</div>

Loading state behavior:

  • Automatically disables the button
  • Shows a spinning loader icon
  • Optionally shows custom loadingText
  • Hides icons while loading

Disabled State

Disabled Buttons

Buttons that are temporarily unavailable

<div className="flex flex-wrap gap-2">
<Button variant="primary" disabled>Primary Disabled</Button>
<Button variant="secondary" disabled>Secondary Disabled</Button>
<Button variant="destructive" disabled>Destructive Disabled</Button>
<Button variant="ghost" disabled>Ghost Disabled</Button>
</div>

Destructive Actions

Use the destructive variant for actions that have permanent consequences like deleting data or canceling subscriptions.

Destructive Actions

Highlight permanent and dangerous actions

<div className="flex flex-wrap gap-2">
<Button variant="destructive">Delete Account</Button>
<Button variant="destructive" leftIcon={<MdDelete />}>
  Remove Item
</Button>
<Button variant="primary">Save Changes</Button>
</div>

You can render a button as a link element using the asChild prop, which is useful for semantic HTML.

Button as Link

Render button styles on a link element

<div className="flex flex-wrap gap-2">
<Button variant="primary" asChild>
  <a href="/dashboard">Go to Dashboard</a>
</Button>
<Button variant="link" asChild>
  <a href="/documentation">Read Docs</a>
</Button>
</div>

Semantic HTML

When using asChild, the button style is applied to the child element. This is useful when you need proper <a> tags for SEO or when integrating with Next.js Link components.

Best Practices

✓

Use action-oriented labels

Button text should clearly describe the action that will occur. Use verbs: "Save changes", "Delete account", "Add to cart", "Submit form".

✓

Provide loading feedback

Always show loading state during async operations to prevent users from clicking multiple times and to provide clear feedback.

✓

Use appropriate size for context

Match button size to importance and density: xs/sm for tables and toolbars, md for forms, lg for prominent CTAs.

✓

Choose the right variant

Use primary for main actions, secondary/destructive for important but not primary actions, ghost/link for subtle actions, and destructive for dangerous operations.

✗

Avoid vague labels

Don't use generic text like "Click here", "OK", "Submit". Be specific about what will happen.

✗

Don't use multiple primary buttons

Only one primary action per interface. Multiple primary buttons create confusion about hierarchy and importance.

✗

Don't use buttons for navigation

Use <Link> or the asChild prop when navigating between pages. Save buttons for actions that change state.

✗

Don't forget disabled states

Disable buttons when actions aren't available. Provide clear feedback about why an action is disabled.

Patterns

// Loading state for async actions
const [saving, setSaving] = useState(false);
<Button loading={saving} onClick={async () => { setSaving(true); await save(); setSaving(false); }}>
  Save changes
</Button>

// Icon buttons — always use leftIcon/rightIcon props, not children
<Button leftIcon={<MdAdd />}>Add item</Button>
<Button rightIcon={<MdChevronRight />}>Next step</Button>

// Destructive confirmation with secondary variant
<Button variant="destructive" onClick={handleDelete}>Delete account</Button>

// Navigate with asChild + Next.js Link
<Button asChild variant="ghost">
  <Link href="/settings">Settings</Link>
</Button>

// Button group — consistent sizes
<div className="flex items-center gap-2">
  <Button variant="secondary" size="sm">Cancel</Button>
  <Button variant="primary" size="sm">Confirm</Button>
</div>

Choosing the right alternative

You want…Use this
Icon-only trigger (toolbar, table row, header bar)IconButton
On/off toggle (notifications, dark mode)Switch
Multi-select opt-in (terms accepted, features enabled)Checkbox
Mutually exclusive choice (plan tier, billing cycle)RadioGroup
Navigation styled like a button<Button asChild><Link href="..."/></Button>
Inline text link (within a paragraph)Button variant="link"
Compact action group (Save / Cancel pair)Pair of Buttons with consistent size
Multi-action toolbarButtonGroup
Floating context action (kebab menu)IconButton inside a Dropdown or Popover

Common Mistakes

// ❌ WRONG — no "outline" or "solid" variants
<Button variant="outline">Cancel</Button>
<Button variant="solid">Submit</Button>
// ✅ CORRECT
<Button variant="secondary">Cancel</Button>
<Button variant="primary">Submit</Button>

// ❌ WRONG — passing an icon as children
<Button><MdAdd /></Button>
// ✅ CORRECT — use leftIcon prop
<Button leftIcon={<MdAdd />}>Add item</Button>

// ❌ WRONG — no "icon" prop on Button (use IconButton instead)
<Button icon={<MdEdit />} />
// ✅ CORRECT — use IconButton for icon-only buttons
<IconButton icon={<MdEdit />} aria-label="Edit" />

// ❌ WRONG — mixing sizes in the same button group
<div><Button size="lg">Cancel</Button><Button size="sm">OK</Button></div>
// ✅ CORRECT — consistent size within a group
<div><Button size="sm">Cancel</Button><Button size="sm">OK</Button></div>

Props

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'destructive' | 'ghost' | 'ghost-body' | 'ghost-destructive' | 'link''primary'Visual style variant
size'xs' | 'sm' | 'md' | 'lg''md'Button size
loadingbooleanfalseShow loading spinner and disable interaction
loadingTextReactNode-Custom text shown during loading state
leftIconReactNode-Icon displayed before the button text
rightIconReactNode-Icon displayed after the button text
asChildbooleanfalseRender as child element using Radix Slot (e.g. for <a> tags)
disabledbooleanfalseDisable the button
classNamestring-Additional className
childrenReactNode-Button content

Accessibility

Keyboard Navigation

Buttons can be activated using the Space or Enter key when focused. Use Tab to navigate between buttons.

Button vs Link

Use a button for actions that affect the current page. Use a link for navigation to different pages or sections.