Button
Triggers an action — submitting a form, opening a dialog, saving a change. Use a link instead when the result is navigation.
<Button variant="primary">Save changes</Button>
<Button>Cancel</Button>Import
import { Button } from "@merid/react";Examples
Variants
primary is the single accent action on a screen. secondary (the default) is for everything else; ghost sits quietly in toolbars and dense rows; danger confirms destructive actions; link looks like inline text but keeps button semantics.
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
<Button variant="link">Link</Button>Sizes
sm is 36px, md 44px (default) and lg 50px tall. Match the size of neighbouring inputs so a row of controls shares one baseline.
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>With icons
leadingIcon and trailingIcon take any node. They are wrapped in aria-hidden spans, so the label alone names the button.
<Button variant="primary" leadingIcon={<PlusIcon />}>New project</Button>
<Button trailingIcon={<ArrowRightIcon />}>Continue</Button>
<Button variant="danger" leadingIcon={<TrashIcon />}>Delete</Button>Loading
loading overlays a spinner, keeps the button's width so the layout does not jump, sets aria-busy and aria-disabled, and swallows clicks. The button stays focusable, so keyboard focus is not lost mid-request.
const [saving, setSaving] = useState(false);
<Button
variant="primary"
loading={saving}
onClick={() => {
setSaving(true);
window.setTimeout(() => setSaving(false), 1500);
}}
>
Save changes
</Button>Disabled
disabled is the native attribute: the button leaves the tab order and ignores clicks. Prefer explaining why an action is unavailable over silently disabling it.
<Button variant="primary" disabled>Publish</Button>
<Button disabled>Export</Button>Full width
fullWidth stretches the button to its container — useful in narrow forms and on mobile.
<Button variant="primary" fullWidth>Create account</Button>As a router link
Use asChild to render your router's link with button styling, so navigation stays a real link:
import Link from "next/link";
<Button asChild variant="primary">
<Link href="/signup">Start free</Link>
</Button>API reference
Button forwards its ref to the <button> and passes every other native button attribute through. type defaults to "button", so a button inside a form does not submit it unless you set type="submit".
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "primary" | "secondary" | "ghost" | "danger" | "link" | "secondary" | Visual style. primary is the single accent action. |
size | "sm" | "md" | "lg" | "md" | Height and padding: sm 36px, md 44px, lg 50px. |
loading | boolean | false | Shows a spinner, keeps the width, sets aria-busy and blocks clicks. |
leadingIcon | ReactNode | – | Icon before the label. Hidden from assistive tech. |
trailingIcon | ReactNode | – | Icon after the label. Hidden from assistive tech. |
fullWidth | boolean | false | Stretches the button to the width of its container. |
asChild | boolean | false | Render the single child element (e.g. a router `<Link>`) styled as this button instead of a `<button>`. Props, ref, handlers and className merge onto the child; its children are wrapped in the button's content span. `disabled`/`loading` become `aria-disabled` and block clicks. Works from server components. |
type | "button" | "submit" | "reset" | "button" | Native button type. |
disabled | boolean | – | Native disabled attribute. |
Styling
| Hook | Values |
|---|---|
.mrd-button | Root <button> |
.mrd-button__content | Wrapper around icons and label (hidden while loading) |
.mrd-button__icon | Leading / trailing icon slot |
.mrd-button__spinner | Spinner shown while loading |
data-variant | primary · secondary · ghost · danger · link |
data-size | sm · md · lg |
data-loading | Present while loading |
data-full-width | Present when fullWidth |
Component variables, set per size: --mrd-button-height, --mrd-button-padding, --mrd-button-font.
.toolbar .mrd-button {
--mrd-button-height: 32px;
--mrd-button-padding: 0 12px;
}Accessibility
- Renders a native
<button>, so Enter and Space activate it and it takes part in forms. - The visible label is the accessible name. For icon-only actions use IconButton, which requires a
label. - While
loading, the button reportsaria-busy="true"andaria-disabled="true"but keeps focus.
| Key | Action |
|---|---|
| Tab | Moves focus to the button. |
| Enter | Activates the button. |
| Space | Activates the button. |
Guidelines
Do
Use one primary button per view, labelled with a verb that says what happens: “Save changes”, “Send invite”.
Avoid
Put two primary buttons side by side, or use vague labels like “OK” and “Submit”.
Do
Use loading while a request is in flight so the button keeps its place and focus.
Avoid
Swap the label for a spinner by hand or disable the button during the request — focus is lost and the width jumps.
Related
- IconButton — square, icon-only actions.
- Link — navigation rather than actions.
- Spinner — the indicator used by
loading.