Button
A clear, responsive action with quiet secondary states.
actioncontrol
Preview
Button preview
Live · keyboard ready
When to use
- Any single action on a page, form, or dialog, such as Save, Continue, or Cancel.
- Actions whose label changes in place, like Save to Saved, where the width should spring instead of jump.
- Short async work where a spinner on the button is enough feedback, via loading.
When not to use
- Use action-button when the button itself should show pending and success states after an async commit.
- Use split-button when one default action has two to five close variants.
- Use hold-to-confirm for destructive actions that need more than a single click.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "button",
"type": "registry:ui",
"title": "Button",
"description": "A clear, responsive action with quiet secondary states.",
"categories": [
"actions"
],
"dependencies": [
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/button/button.tsx",
"type": "registry:component",
"target": "@components/manicat/button/button.tsx"
},
{
"path": "registry/components/button/button.module.css",
"type": "registry:component",
"target": "@components/manicat/button/button.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/button",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/button",
"markdown": "https://uiarc.dev/components/button/markdown",
"tags": [
"action",
"control"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { Button } from "@/components/manicat/button/button";
export function ButtonExample() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button>Save changes</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost">Learn more</Button>
<Button variant="danger">Delete project</Button>
</div>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Destructive action Danger is reserved for the one irreversible choice; Cancel stays the easy way out.
Destructive action
Danger is reserved for the one irreversible choice; Cancel stays the easy way out. preview
Live · keyboard ready
Delete project
This removes 12 deployments and every environment variable. It can't be undone.
destructive-action-danger-is-reserved-for-the-one-irreversible-choice-cancel-stays-the-easy-way-out-.tsxtsx
"use client";
import { Download, Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/manicat/button/button";
export function ButtonDestructiveExample() {
return (
<div className="grid w-full max-w-sm gap-4 rounded-[var(--radius-panel)] border border-(color:--border) bg-(--surface) p-5">
<div>
<h4 className="text-base font-medium text-(color:--foreground)">Delete project</h4>
<p className="mt-1 text-sm text-(color:--text-muted)">This removes 12 deployments and every environment variable. It can't be undone.</p>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary">Cancel</Button>
<Button variant="danger">
<Trash2 className="size-4" /> Delete
</Button>
</div>
</div>
);
}
Page header actions One primary action, a secondary one and a quiet ghost button, the usual hierarchy of a page header.
Page header actions
One primary action, a secondary one and a quiet ghost button, the usual hierarchy of a page header. preview
Live · keyboard ready
Invoices
42 invoices this quarter
page-header-actions-one-primary-action-a-secondary-one-and-a-quiet-ghost-button-the-usual-hierarchy-of-a-page-header-.tsxtsx
Async submit The loading state keeps the label and blocks double submits while a request is in flight.
Async submit
The loading state keeps the label and blocks double submits while a request is in flight. preview
Live · keyboard ready
async-submit-the-loading-state-keeps-the-label-and-blocks-double-submits-while-a-request-is-in-flight-.tsxtsx
"use client";
import { useState } from "react";
import { Button } from "@/components/manicat/button/button";
export function ButtonLoadingExample() {
const [loading, setLoading] = useState(false);
const [saved, setSaved] = useState(false);
function save() {
setLoading(true);
setSaved(false);
setTimeout(() => {
setLoading(false);
setSaved(true);
}, 1400);
}
return (
<div className="flex items-center gap-3">
<Button loading={loading} onClick={save}>
Save changes
</Button>
{saved && <span className="text-sm text-(color:--text-muted)">Saved just now</span>}
</div>
);
}
Sizes Small for dense toolbars, medium by default, large for hero calls to action.
Sizes
Small for dense toolbars, medium by default, large for hero calls to action. preview
Live · keyboard ready
sizes-small-for-dense-toolbars-medium-by-default-large-for-hero-calls-to-action-.tsxtsx
"use client";
import { Button } from "@/components/manicat/button/button";
export function ButtonSizesExample() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| Enter / Space | Activates the button. |
Accessibility
- Renders a native button, so role and focus come for free.
- Loading uses aria-busy and aria-disabled instead of disabled, so keyboard focus is not lost mid-action.
- Icon-only buttons need an aria-label.
- Disabled primary buttons fade as a whole. Disabled secondary and danger buttons keep their border and take a muted fill and label, and disabled ghost buttons take a muted label, so an unavailable action still reads as a button.
Motion
- Presses scale to about 0.97 on a snappy spring; icon-sized buttons press slightly deeper.
- A new label crossfades with a short blur while the width springs to fit.
- Reduced motion drops the press scale and swaps labels with a plain fade.
Responsive behavior
- Size is fixed by the size prop; the button never changes layout by breakpoint, so pick lg for primary touch targets.
- Hover styles apply only on hover-capable fine pointers, so taps on touch screens do not stick in a hover state.
Performance
- A ResizeObserver measures the label so the width can spring; one per button is cheap, but avoid hundreds in a long list.
- Label changes animate opacity, transform, and a small blur only, with no layout thrash beyond the width spring.
Notes for AI
- Default choice for any single action. Use action-button for dense icon toolbars and split-button when one action has close alternatives.
- Pass a changing label (Save → Saved) as children to get the width morph for free.
- Wrap in a Radix trigger with asChild; the press scale turns off automatically for popup anchors.