# Button

Triggers an action with clear hierarchy and tactile feedback.

Category: Actions
Status: stable
Metadata: /docs/components/button/metadata

## Recommendations

- Use default or brand for the main action. Keep supporting actions quieter.
- Give icon buttons an aria-label. Hide decorative icons from assistive technology.
- Keep the label when loading. Loading blocks activation and preserves keyboard focus.
- Tab moves focus. Enter or Space activates an enabled button.
- Button supports Server Components. Add event handlers in a Client Component.

## Usage boundaries

- Use a link for navigation, including when composing with render.
- Use Toggle for a persistent pressed state.
- Use Alert Dialog when a destructive action needs confirmation.

## API

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| variant | "default" \| "brand" \| "secondary" \| "outline" \| "ghost" \| "link" \| "destructive" \| "destructive-outline" | "default" | Sets the semantic appearance and interaction treatment. |
| size | "xs" \| "sm" \| "default" \| "lg" \| "xl" \| "icon-xs" \| "icon-sm" \| "icon" \| "icon-lg" \| "icon-xl" | "default" | Sets control dimensions. Icon sizes require an accessible name. |
| loading | boolean | false | Shows progress while preserving the button width. |
| disabled | boolean | false | Prevents interaction and communicates the disabled state. |
| children | ReactNode | — | Visible label or icon content. Icon-only buttons need aria-label or aria-labelledby. |
| render | ReactElement \| RenderFunction | — | Composes button behavior onto another element. |
| nativeButton | boolean | true | Defaults to true; set false when render produces a non-button element. |
| type | "button" \| "submit" \| "reset" | "button" | Sets the native button type when render is not used. |


Import the public component and exported prop types from `@mizen-labs/ui`. See the rendered example at `/docs/components/button`.

## Usage

```tsx
import { Button } from "@mizen-labs/ui";
```

## Action weights

The brand button leads, an outline button offers a second path, and a ghost button lets people skip.

```tsx
import { Button, Inline } from "@mizen-labs/ui";

export function BrandAction() {
	return (
		<Inline>
			<Button variant="brand">Invite</Button>
			<Button variant="outline">Copy link</Button>
			<Button variant="ghost">Skip</Button>
		</Inline>
	);
}
```

## Icon action

Name the action with aria-label. Hide the decorative SVG from assistive technology.

```tsx
import { Button } from "@mizen-labs/ui";

import { IconPlus } from "@tabler/icons-react";

export function IconAction() {
	return (
		<Button aria-label="Add project" size="icon-sm">
			<IconPlus aria-hidden="true" size="1em" />
		</Button>
	);
}
```

## Loading

Keep the action label stable. Loading blocks repeated activation and sets aria-busy.

```tsx
import { Button } from "@mizen-labs/ui";

export function LoadingAction() {
	return (
		<Button loading variant="brand">
			Save changes
		</Button>
	);
}
```

## Disabled

Explain unavailable actions near the control. Disabled buttons leave the keyboard tab order.

```tsx
import { Button } from "@mizen-labs/ui";

export function DisabledAction() {
	return <Button disabled>Publish project</Button>;
}
```

## Release review

Northstar / Production

```tsx
"use client";

import { Badge, Button, Heading, Inline, Stack, Text } from "@mizen-labs/ui";

import { useCallback, useState } from "react";

export function ButtonReleaseReviewExample1() {
	const [status, setStatus] = useState("Ready for review");
	const publish = useCallback(
		() => setStatus("Release published in this preview."),
		[]
	);
	const save = useCallback(() => setStatus("Draft saved in this preview."), []);
	return (
		<Stack gap="4">
			<Stack gap="1">
				<Inline>
					<Heading level={4}>A calmer workspace</Heading>
					<Badge variant="outline">Draft</Badge>
				</Inline>
				<Text size="ui" tone="muted">
					The release notes are ready. Save your work or publish the update.
				</Text>
			</Stack>
			<Inline>
				<Button onClick={publish} variant="brand">
					Publish update
				</Button>
				<Button onClick={save} variant="outline">
					Save draft
				</Button>
			</Inline>
			<Text aria-live="polite" size="ui" tone="muted">
				{status}
			</Text>
		</Stack>
	);
}
```

## Action hierarchy

Use one strong action. Keep supporting commands quieter.

```tsx
"use client";

import { Button, Inline, Stack } from "@mizen-labs/ui";

export function ButtonActionHierarchyExample2() {
	return (
		<Stack gap="4">
			<Inline>
				<Button>Default</Button>
				<Button variant="brand">Brand</Button>
				<Button variant="secondary">Secondary</Button>
				<Button variant="outline">Outline</Button>
				<Button variant="ghost">Ghost</Button>
				<Button variant="link">Link</Button>
			</Inline>
		</Stack>
	);
}
```

## Destructive actions

Give destructive commands their own place, away from the main action.

```tsx
"use client";

import { Button, Inline, Stack } from "@mizen-labs/ui";

export function ButtonDestructiveActionsExample3() {
	return (
		<Stack gap="4">
			<Inline>
				<Button variant="destructive">Destructive</Button>
				<Button variant="destructive-outline">Delete</Button>
			</Inline>
		</Stack>
	);
}
```

## Action states

Loading and unavailable actions keep their dimensions and labels.

```tsx
"use client";

import { Button, Inline, Stack } from "@mizen-labs/ui";

import { IconPlus } from "@tabler/icons-react";

function PlusIcon() {
	return <IconPlus aria-hidden="true" focusable="false" size="1em" />;
}

export function ButtonActionStatesExample4() {
	return (
		<Stack gap="4">
			<Inline>
				<Button loading>Saving</Button>
				<Button disabled>Disabled</Button>
				<Button aria-label="Add" size="icon-sm">
					<PlusIcon />
				</Button>
			</Inline>
		</Stack>
	);
}
```

## Control sizes



```tsx
"use client";

import { Button, Inline, Stack } from "@mizen-labs/ui";

export function ButtonControlSizesExample5() {
	return (
		<Stack gap="4">
			<Inline>
				<Button size="default">Default size</Button>
				<Button size="xs">Extra small</Button>
				<Button size="sm">Small</Button>
				<Button size="lg">Large</Button>
				<Button size="xl">Extra large</Button>
			</Inline>
		</Stack>
	);
}
```

## Icon actions

Use a clear accessible name for each icon-only action.

```tsx
"use client";

import { Button, Inline, Stack } from "@mizen-labs/ui";

import { IconPlus } from "@tabler/icons-react";

function PlusIcon() {
	return <IconPlus aria-hidden="true" focusable="false" size="1em" />;
}

export function ButtonIconActionsExample6() {
	return (
		<Stack gap="4">
			<Inline>
				<Button aria-label="Icon" size="icon">
					<PlusIcon />
				</Button>
				<Button aria-label="Extra small icon" size="icon-xs">
					<PlusIcon />
				</Button>
				<Button aria-label="Small icon" size="icon-sm">
					<PlusIcon />
				</Button>
				<Button aria-label="Large icon" size="icon-lg">
					<PlusIcon />
				</Button>
				<Button aria-label="Extra large icon" size="icon-xl">
					<PlusIcon />
				</Button>
			</Inline>
		</Stack>
	);
}
```

## Example code

Render `ButtonSpecimen` from the first file. Keep supporting files at the shown relative paths. These are the full preview sources, including demo layout and sample data.

### components/[slug]/specimens/button-specimen.tsx

```tsx
"use client";

import { Badge, Button, Heading, Inline, Stack, Text } from "@mizen-labs/ui";
import { props } from "@stylexjs/stylex";
import { IconPlus } from "@tabler/icons-react";
import { useCallback, useState } from "react";
import { specimenStyles } from "./specimen.styles";
import { SpecimenSection } from "./specimen-section";

function PlusIcon() {
	return <IconPlus aria-hidden="true" focusable="false" size="1em" />;
}

export function ButtonSpecimen() {
	const [status, setStatus] = useState("Ready for review");
	const publish = useCallback(
		() => setStatus("Release published in this preview."),
		[]
	);
	const save = useCallback(() => setStatus("Draft saved in this preview."), []);
	return (
		<div {...props(specimenStyles.examples)}>
			<SpecimenSection
				description="Northstar / Production"
				primary
				title="Release review"
			>
				<Stack gap="1">
					<Inline>
						<Heading level={4}>A calmer workspace</Heading>
						<Badge variant="outline">Draft</Badge>
					</Inline>
					<Text size="ui" tone="muted">
						The release notes are ready. Save your work or publish the update.
					</Text>
				</Stack>
				<Inline>
					<Button onClick={publish} variant="brand">
						Publish update
					</Button>
					<Button onClick={save} variant="outline">
						Save draft
					</Button>
				</Inline>
				<Text aria-live="polite" size="ui" tone="muted">
					{status}
				</Text>
			</SpecimenSection>
			<SpecimenSection
				description="Use one strong action. Keep supporting commands quieter."
				title="Action hierarchy"
			>
				<Inline>
					<Button>Default</Button>
					<Button variant="brand">Brand</Button>
					<Button variant="secondary">Secondary</Button>
					<Button variant="outline">Outline</Button>
					<Button variant="ghost">Ghost</Button>
					<Button variant="link">Link</Button>
				</Inline>
			</SpecimenSection>
			<SpecimenSection
				description="Give destructive commands their own place, away from the main action."
				title="Destructive actions"
			>
				<Inline>
					<Button variant="destructive">Destructive</Button>
					<Button variant="destructive-outline">Delete</Button>
				</Inline>
			</SpecimenSection>
			<SpecimenSection
				description="Loading and unavailable actions keep their dimensions and labels."
				title="Action states"
			>
				<Inline>
					<Button loading>Saving</Button>
					<Button disabled>Disabled</Button>
					<Button aria-label="Add" size="icon-sm">
						<PlusIcon />
					</Button>
				</Inline>
			</SpecimenSection>
			<SpecimenSection title="Control sizes">
				<Inline>
					<Button size="default">Default size</Button>
					<Button size="xs">Extra small</Button>
					<Button size="sm">Small</Button>
					<Button size="lg">Large</Button>
					<Button size="xl">Extra large</Button>
				</Inline>
			</SpecimenSection>
			<SpecimenSection
				description="Use a clear accessible name for each icon-only action."
				title="Icon actions"
			>
				<Inline>
					<Button aria-label="Icon" size="icon">
						<PlusIcon />
					</Button>
					<Button aria-label="Extra small icon" size="icon-xs">
						<PlusIcon />
					</Button>
					<Button aria-label="Small icon" size="icon-sm">
						<PlusIcon />
					</Button>
					<Button aria-label="Large icon" size="icon-lg">
						<PlusIcon />
					</Button>
					<Button aria-label="Extra large icon" size="icon-xl">
						<PlusIcon />
					</Button>
				</Inline>
			</SpecimenSection>
		</div>
	);
}
```

### components/[slug]/specimens/specimen.styles.ts

```tsx
import { colors } from "@mizen-labs/ui/colors.stylex";
import { shape } from "@mizen-labs/ui/shape.stylex";
import { space } from "@mizen-labs/ui/spacing.stylex";
import { create } from "@stylexjs/stylex";

export const specimenStyles = create({
	examples: {
		display: "grid",
		gap: space["8"],
		gridTemplateColumns: "minmax(0, 1fr)",
		minInlineSize: 0,
	},
	gallery: {
		backgroundColor: colors.workarea,
		borderRadius: shape.controlRadiusLarge,
		boxSizing: "border-box",
		display: "grid",
		gap: space["8"],
		gridTemplateColumns: "minmax(0, 1fr)",
		minInlineSize: 0,
		padding: space["6"],
		width: "100%",
	},
	identity: {
		alignItems: "center",
		display: "grid",
		gap: space["3"],
		gridTemplateColumns: "auto minmax(0, 1fr)",
		minInlineSize: 0,
	},
});
```

### components/[slug]/specimens/specimen-section.tsx

```tsx
import { Heading, Stack, Text } from "@mizen-labs/ui";
import type { ReactNode } from "react";
import { PreviewCanvas, PreviewWorkspace } from "../../preview-workspace";

export function SpecimenSection({
	title,
	description,
	children,
	primary = false,
	canvas = true,
}: {
	title: string;
	description?: string;
	children: ReactNode;
	primary?: boolean;
	canvas?: boolean;
}) {
	if (primary) {
		return (
			<PreviewWorkspace canvas={canvas} description={description} title={title}>
				{children}
			</PreviewWorkspace>
		);
	}
	return (
		<section aria-label={title}>
			<Stack gap="4">
				<Stack gap="1">
					<Heading level={3}>{title}</Heading>
					{description ? (
						<Text size="ui" tone="muted">
							{description}
						</Text>
					) : null}
				</Stack>
				{canvas ? (
					<PreviewCanvas>{children}</PreviewCanvas>
				) : (
					<Stack gap="4">{children}</Stack>
				)}
			</Stack>
		</section>
	);
}
```

### components/preview-workspace.tsx

```tsx
import { Heading, Stack, Text } from "@mizen-labs/ui";
import { props } from "@stylexjs/stylex";
import type { ReactNode } from "react";
import { previewWorkspaceStyles } from "./preview-workspace.styles";

export function PreviewCanvas({ children }: { children: ReactNode }) {
	return <div {...props(previewWorkspaceStyles.canvas)}>{children}</div>;
}

export function PreviewWorkspace({
	title,
	description,
	children,
	canvas = true,
	titleId,
	testId,
}: {
	title: string;
	description?: string;
	children: ReactNode;
	canvas?: boolean;
	titleId?: string;
	testId?: string;
}) {
	return (
		<section
			aria-label={title}
			data-testid={testId}
			{...props(previewWorkspaceStyles.workarea)}
		>
			<Stack gap="1">
				<Heading id={titleId} level={3}>
					{title}
				</Heading>
				{description ? (
					<Text size="ui" tone="muted">
						{description}
					</Text>
				) : null}
			</Stack>
			{canvas ? (
				<PreviewCanvas>{children}</PreviewCanvas>
			) : (
				<Stack gap="4">{children}</Stack>
			)}
		</section>
	);
}
```

### components/preview-workspace.styles.ts

```tsx
import { colors } from "@mizen-labs/ui/colors.stylex";
import { conditions } from "@mizen-labs/ui/conditions.stylex";
import { shadows } from "@mizen-labs/ui/shadows.stylex";
import { shape } from "@mizen-labs/ui/shape.stylex";
import { space } from "@mizen-labs/ui/spacing.stylex";
import { create } from "@stylexjs/stylex";

export const previewWorkspaceStyles = create({
	alignCenter: { textAlign: "center" },
	alignEnd: { textAlign: "end" },
	canvas: {
		backgroundColor: colors.surface,
		borderColor: colors.border,
		borderRadius: shape.controlRadiusLarge,
		borderStyle: "solid",
		borderWidth: 1,
		boxShadow: shadows.surface,
		boxSizing: "border-box",
		display: "grid",
		gap: space["4"],
		gridTemplateColumns: "minmax(0, 1fr)",
		minInlineSize: 0,
		overflowWrap: "break-word",
		padding: { default: space["3"], [conditions.sm]: space["6"] },
	},
	copy: { margin: 0, overflowWrap: "anywhere" },
	underline: { textDecoration: "underline" },
	workarea: {
		backgroundColor: colors.workarea,
		borderRadius: shape.controlRadiusLarge,
		boxSizing: "border-box",
		display: "grid",
		gap: space["4"],
		gridTemplateColumns: "minmax(0, 1fr)",
		minInlineSize: 0,
		padding: { default: space["3"], [conditions.sm]: space["6"] },
	},
});
```
