On this page

Actions

Button

A labelled control for actions in an interface.

Button triggers an action, such as saving a form, opening a dialog, or deleting an item. Use Link when the interaction takes someone to another URL or route.

Button: Basic
import { Button } from '@luke-ui/react/button';

export default () => {
	return <Button>Save changes</Button>;
};

Best practices

GuidancePractices
DoUse Button for an action, such as saving, submitting, or opening a dialog.
Don'tNavigate from a Button. It loses native link semantics and browser link behaviours. Use a Link with appearance="button" instead.
Don'tPut a high Button on a solid fill of its own colour: accent.solid for a neutral tone or danger.solid for a danger tone. Its fill blends into the background. Place the action on a contrasting surface instead.

Size

medium is the default size. Use small in dense toolbars, tables, and other compact interfaces. Keep related controls at the same size.

Button: Size
small
medium
import { Button } from '@luke-ui/react/button';
import { Comparison, ComparisonItem } from '#docs';

export default () => {
	return (
		<Comparison>
			<ComparisonItem label="small">
				<Button size="small">Save changes</Button>
			</ComparisonItem>
			<ComparisonItem label="medium">
				<Button size="medium">Save changes</Button>
			</ComparisonItem>
		</Comparison>
	);
};

Appearance

button is the default appearance. It has control sizing, truncates its label, and supports content slots. Set appearance="text" when an action should use text-link presentation. Text Buttons wrap and do not support a size, block layout, or content slots. Text Buttons are neutral at low and standard prominence and use the accent colour at high prominence. Low prominence has no underline at rest and gains one on hover and press. Standard and high prominence are underlined at rest and lose the underline on hover and press. Keyboard focus shows the focus ring.

Button: Appearance
Button
Text
import { Button } from '@luke-ui/react/button';
import { Comparison, ComparisonItem } from '#docs';

export default () => {
	return (
		<Comparison>
			<ComparisonItem label="Button">
				<Button>Save changes</Button>
			</ComparisonItem>
			<ComparisonItem label="Text">
				<Button appearance="text">Save changes</Button>
			</ComparisonItem>
		</Comparison>
	);
};

Tone

neutral is the default tone for ordinary actions. Set tone="danger" for destructive actions.

Button: Tone
Neutral
Danger
import { Button } from '@luke-ui/react/button';
import { Comparison, ComparisonItem } from '#docs';

export default () => {
	return (
		<Comparison>
			<ComparisonItem label="Neutral">
				<Button tone="neutral">Save changes</Button>
			</ComparisonItem>
			<ComparisonItem label="Danger">
				<Button tone="danger">Delete draft</Button>
			</ComparisonItem>
		</Comparison>
	);
};

Prominence

standard is the default. Choose low for quiet secondary actions and high for the primary action in a group. For ordinary actions, high prominence uses the theme’s primary accent treatment.

Button: Prominence
Low
Standard
High
import { Button } from '@luke-ui/react/button';
import { Comparison, ComparisonItem } from '#docs';

export default () => {
	return (
		<Comparison>
			<ComparisonItem label="Low">
				<Button prominence="low">Save changes</Button>
			</ComparisonItem>
			<ComparisonItem label="Standard">
				<Button>Save changes</Button>
			</ComparisonItem>
			<ComparisonItem label="High">
				<Button prominence="high">Save changes</Button>
			</ComparisonItem>
		</Comparison>
	);
};

Presentation reference

Button: Presentation reference
Button
Neutral · Low
Neutral · Standard
Neutral · High
Danger · Low
Danger · Standard
Danger · High
Text
Neutral · Low
Neutral · Standard
Neutral · High
Danger · Low
Danger · Standard
import { Button } from '@luke-ui/react/button';
import { Stack } from '@luke-ui/react/stack';
import { Text } from '@luke-ui/react/text';
import { Comparison, ComparisonItem } from '#docs';

export default () => {
	return (
		<Stack gap="space.24">
			<Stack gap="space.8">
				<Text typography="label">Button</Text>
				<Comparison>
					<ComparisonItem label="Neutral · Low">
						<Button prominence="low">Button</Button>
					</ComparisonItem>
					<ComparisonItem label="Neutral · Standard">
						<Button>Button</Button>
					</ComparisonItem>
					<ComparisonItem label="Neutral · High">
						<Button prominence="high">Button</Button>
					</ComparisonItem>
					<ComparisonItem label="Danger · Low">
						<Button prominence="low" tone="danger">
							Button
						</Button>
					</ComparisonItem>
					<ComparisonItem label="Danger · Standard">
						<Button tone="danger">Button</Button>
					</ComparisonItem>
					<ComparisonItem label="Danger · High">
						<Button prominence="high" tone="danger">
							Button
						</Button>
					</ComparisonItem>
				</Comparison>
			</Stack>
			<Stack gap="space.8">
				<Text typography="label">Text</Text>
				<Comparison>
					<ComparisonItem label="Neutral · Low">
						<Button appearance="text" prominence="low">
							Button
						</Button>
					</ComparisonItem>
					<ComparisonItem label="Neutral · Standard">
						<Button appearance="text">Button</Button>
					</ComparisonItem>
					<ComparisonItem label="Neutral · High">
						<Button appearance="text" prominence="high">
							Button
						</Button>
					</ComparisonItem>
					<ComparisonItem label="Danger · Low">
						<Button appearance="text" prominence="low" tone="danger">
							Button
						</Button>
					</ComparisonItem>
					<ComparisonItem label="Danger · Standard">
						<Button appearance="text" tone="danger">
							Button
						</Button>
					</ComparisonItem>
				</Comparison>
			</Stack>
		</Stack>
	);
};

Inline sizing

Pass inlineSize="full" when the button should fill the inline size of its container. A typical case is a primary action at the end of a narrow form or panel.

Button: Inline sizing
import { Button } from '@luke-ui/react/button';

export default () => {
	return <Button inlineSize="full">Continue</Button>;
};

Content slots

Use startContent or endContent for non-interactive adornments beside the label, such as an icon, badge, count, or keyboard hint. Nested interactive controls are unsupported. Button sizes nested icons for you, so an Icon needs no size prop. Pass one only to override it. Use IconButton only when the icon is familiar without a text label.

Button: Content slots
import { Button } from '@luke-ui/react/button';
import { Cluster } from '@luke-ui/react/cluster';
import { Icon } from '@luke-ui/react/icon';
import { Kbd } from '@luke-ui/react/kbd';

export default () => {
	return (
		<Cluster alignItems="center" gap="space.16">
			<Button startContent={<Icon name="add" />}>Add item</Button>
			<Button endContent={<Kbd>⌘S</Kbd>}>Save</Button>
		</Cluster>
	);
};

Pending and Actions

onPress handles the interaction. pressAction performs the resulting operation. When both are set, onPress runs first, then pressAction.

Prefer a React form Action when the operation is a form submission. Pass the Action to Luke UI's Form and the form's pending state to Button. This example uses React's useActionState.

Button: Form action
import { Button } from '@luke-ui/react/button';
import { Form } from '@luke-ui/react/form';
import { useActionState } from 'react';

export default () => {
	const [, formAction, isPending] = useActionState(save, null);

	return (
		<Form action={formAction}>
			<Button isPending={isPending} type="submit">
				Save
			</Button>
		</Form>
	);
};

async function save(_state: null, _formData: FormData) {
	await new Promise((resolve) => {
		setTimeout(resolve, 1200);
	});

	return null;
}

Use pressAction for Button-owned operations that are not form submissions. Combining pressAction with type="submit" is usually the wrong pattern, because both paths may run.

pressAction creates pending state automatically. Explicit isPending shows a spinner immediately. An Action-owned spinner waits briefly so a fast Action never flashes one.

Button: Pending
import { Button } from '@luke-ui/react/button';

export default () => {
	return <Button pressAction={save}>Save changes</Button>;
};

async function save() {
	await new Promise((resolve) => {
		setTimeout(resolve, 1200);
	});
}

Set isPending for externally owned pending state, such as a parent mutation or router transition. Pending buttons stay focusable and ignore further presses.

Button: External pending
import { Button } from '@luke-ui/react/button';

export default () => {
	return <Button isPending>Save changes</Button>;
};

Unexpected failures from pressAction use React’s normal Action and Error Boundary behaviour. Handle expected domain errors inside the Action.

Button: Action error
import { Button } from '@luke-ui/react/button';
import { Cluster } from '@luke-ui/react/cluster';
import { Stack } from '@luke-ui/react/stack';
import { Text } from '@luke-ui/react/text';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';

export default () => {
	return (
		<ErrorBoundary FallbackComponent={ErrorFallback}>
			<Cluster>
				<Button pressAction={save}>Save changes</Button>
			</Cluster>
		</ErrorBoundary>
	);
};

async function save() {
	await new Promise((resolve) => {
		setTimeout(resolve, 400);
	});
	throw new Error('Your changes were not saved.');
}

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
	const message = error instanceof Error ? error.message : 'Something went wrong';

	return (
		<Stack gap="space.12">
			<Text color="danger" role="alert">
				{message}
			</Text>
			<Cluster>
				<Button onPress={resetErrorBoundary}>Try again</Button>
			</Cluster>
		</Stack>
	);
}

Disabled

Set isDisabled only when the action is unavailable. Disabled buttons cannot receive focus or respond to presses.

Button: Disabled
import { Button } from '@luke-ui/react/button';

export default () => {
	return <Button isDisabled>Edit profile</Button>;
};

Accessibility

The visible label provides the accessible name, so most buttons do not need aria-label. Write a label that describes the action, such as “Save changes” or “Delete account”, rather than a vague label such as “OK”.

The spinner is hidden from assistive technology. Pending state is announced through the button’s pending semantics, so the label does not need to change while pending.

API

ButtonProps also accepts compatible DOM and ARIA attributes and event handlers for its rendered element.

NameTypeDefault
appearance
"button" |⁠ "text"button

Sets whether the control uses button or text-link styling.

endContent
ReactNode—

Non-interactive adornment shown after the label.

inlineSize
'auto' |⁠ 'full'auto

Use full to fill the available inline size.

isPending
booleanfalse

Externally owned pending state. When true, the button is non-interactive and shows a spinner immediately. Prefer pressAction for Button-owned operations.

pressAction
—

Button-owned operation run as a React Action. The button becomes pending automatically until the Action settles. onPress handles the interaction. pressAction performs the resulting operation. Prefer a native <form action> when the operation is a form submission.

prominence
standard

Controls the strength of the control's visual treatment, from low to high.

size
'small' |⁠ 'medium'medium

Sets the size of the control.

startContent
ReactNode—

Non-interactive adornment shown before the label.

tone
"danger" |⁠ "neutral"neutral

Sets the visual tone. Use danger for destructive actions.