Button
Primary, secondary, ghost, and danger buttons in three sizes with icon support.
Variants
Primary, secondary, ghost, and danger.
function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button>Get started</Button>
<Button variant="secondary">Continue</Button>
<Button variant="ghost">Mark as done</Button>
<Button variant="danger">Delete</Button>
</div>
);
}function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button>Get started</Button>
<Button variant="secondary">Continue</Button>
<Button variant="ghost">Mark as done</Button>
<Button variant="danger">Delete</Button>
</div>
);
}Sizes
Medium, small, and extra small.
function ButtonSizes() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="medium">Medium</Button>
<Button size="small">Small</Button>
<Button size="xs">Extra small</Button>
</div>
);
}function ButtonSizes() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="medium">Medium</Button>
<Button size="small">Small</Button>
<Button size="xs">Extra small</Button>
</div>
);
}Icons
Leading, trailing, and icon-only.
function ButtonIcons() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button leadingIcon={RiAddLine}>New project</Button>
<Button variant="secondary" trailingIcon={RiArrowRightLine}>
Continue
</Button>
<Button variant="ghost" leadingIcon={RiCheckLine}>
Approve
</Button>
<Button variant="danger" leadingIcon={RiDeleteBinLine} iconOnly aria-label="Delete" />
</div>
);
}function ButtonIcons() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button leadingIcon={RiAddLine}>New project</Button>
<Button variant="secondary" trailingIcon={RiArrowRightLine}>
Continue
</Button>
<Button variant="ghost" leadingIcon={RiCheckLine}>
Approve
</Button>
<Button variant="danger" leadingIcon={RiDeleteBinLine} iconOnly aria-label="Delete" />
</div>
);
}Disabled
Every variant in its disabled state.
function ButtonDisabled() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button disabled>Primary</Button>
<Button variant="secondary" disabled>
Secondary
</Button>
<Button variant="ghost" disabled>
Ghost
</Button>
<Button variant="danger" disabled>
Danger
</Button>
</div>
);
}function ButtonDisabled() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button disabled>Primary</Button>
<Button variant="secondary" disabled>
Secondary
</Button>
<Button variant="ghost" disabled>
Ghost
</Button>
<Button variant="danger" disabled>
Danger
</Button>
</div>
);
}Installation
npx shadcn@latest add https://boardcn.dev/r/button.jsonnpx shadcn@latest add https://boardcn.dev/r/button.jsonBoardCN dependencies
The CLI installs these for you — you do not need to add them yourself.
Source
The file the CLI copies into your project.
import type {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
ComponentType,
ReactNode,
Ref,
} from "react";
import { cx, sortCx } from "@/utils/cx";
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
type ButtonSize = "medium" | "small" | "xs";
type IconComponent = ComponentType<{
className?: string;
"aria-hidden"?: boolean | "true" | "false";
}>;
export interface ButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
variant?: ButtonVariant;
size?: ButtonSize;
iconOnly?: boolean;
leadingIcon?: IconComponent;
trailingIcon?: IconComponent;
children?: ReactNode;
ref?: Ref<HTMLButtonElement>;
}
export interface ButtonLinkProps
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "children"> {
variant?: ButtonVariant;
size?: ButtonSize;
iconOnly?: boolean;
leadingIcon?: IconComponent;
trailingIcon?: IconComponent;
children?: ReactNode;
ref?: Ref<HTMLAnchorElement>;
}
const styles = sortCx({
base: [
"inline-flex items-center justify-center gap-0.5 whitespace-nowrap overflow-hidden",
"font-sans select-none cursor-pointer",
"button-press-motion",
"outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-focus-ring",
"disabled:cursor-not-allowed aria-disabled:cursor-not-allowed",
].join(" "),
// Base shape per size (used when label is present OR medium icon-only).
size: {
medium: "h-9 rounded-2lg p-2 text-body-medium",
small: "h-8 rounded-lg px-2 py-1.5 text-body-medium",
xs: "h-6 rounded-sm px-2 text-caption-1-semibold",
},
// Icon-only override:
// Medium → keep p-2; w expands from content (8+20+8 = 36) → square.
// and zero the padding; the inner flex centers the 18px icon.
// Xs → forces 24×24, content-centered — used for the calendar
// template's edit-icon buttons (timezone/participants/reminder).
iconOnlySize: {
medium: "", // base size already produces 36×36 with a 20px icon
small: "size-8 p-0", // hard 32×32, content-centered
xs: "size-6 p-0", // hard 24×24, content-centered
},
icon: {
medium: "size-5 shrink-0", // 20px
small: "size-[18px] shrink-0", // 18px
xs: "size-3.5 shrink-0", // 14px
},
label: {
medium: "inline-flex items-center justify-center px-1 shrink-0", // px=4
small: "inline-flex items-center justify-center px-0.5 shrink-0", // px=2
xs: "inline-flex items-center justify-center px-0.5 shrink-0", // px=2
},
variant: {
primary: [
"bg-button-primary text-text-white shadow-xs",
"disabled:text-button-primary-disabled-foreground disabled:shadow-none",
"aria-disabled:text-button-primary-disabled-foreground aria-disabled:shadow-none",
].join(" "),
danger: [
"bg-button-danger text-text-white shadow-xs",
"disabled:text-foreground-disabled-danger disabled:shadow-none",
"aria-disabled:text-foreground-disabled-danger aria-disabled:shadow-none",
].join(" "),
secondary: [
"bg-background-primary-default text-text-primary",
"border border-border-button-default shadow-xs",
"hover:bg-background-primary-hover hover:border-border-button-hover",
"active:bg-background-primary-active active:border-border-button-active",
"disabled:bg-background-primary-disabled disabled:border-border-button-default disabled:text-text-tertiary disabled:shadow-none",
"aria-disabled:bg-background-primary-disabled aria-disabled:border-border-button-default aria-disabled:text-text-tertiary aria-disabled:shadow-none",
].join(" "),
ghost: [
"bg-button-ghost-background text-button-ghost-foreground",
"hover:bg-button-ghost-hover active:bg-button-ghost-active",
"disabled:bg-button-ghost-disabled disabled:text-button-ghost-disabled-foreground disabled:shadow-none",
"aria-disabled:bg-button-ghost-disabled aria-disabled:text-button-ghost-disabled-foreground aria-disabled:shadow-none",
].join(" "),
},
});
export function Button({
variant = "primary",
size = "medium",
iconOnly = false,
leadingIcon: Leading,
trailingIcon: Trailing,
children,
className,
type = "button",
ref,
...props
}: ButtonProps) {
return (
<button
ref={ref}
type={type}
className={cx(
styles.base,
styles.size[size],
styles.variant[variant],
iconOnly && styles.iconOnlySize[size],
className,
)}
{...props}
>
{Leading ? <Leading className={styles.icon[size]} aria-hidden /> : null}
{!iconOnly && children !== undefined && children !== null && (
<span className={styles.label[size]}>{children}</span>
)}
{!iconOnly && Trailing ? (
<Trailing className={styles.icon[size]} aria-hidden />
) : null}
</button>
);
}
/** Anchor counterpart to Button for navigational actions. */
export function ButtonLink({
variant = "primary",
size = "medium",
iconOnly = false,
leadingIcon: Leading,
trailingIcon: Trailing,
children,
className,
ref,
...props
}: ButtonLinkProps) {
return (
<a
ref={ref}
className={cx(
styles.base,
styles.size[size],
styles.variant[variant],
iconOnly && styles.iconOnlySize[size],
className,
)}
{...props}
>
{Leading ? <Leading className={styles.icon[size]} aria-hidden /> : null}
{!iconOnly && children !== undefined && children !== null && (
<span className={styles.label[size]}>{children}</span>
)}
{!iconOnly && Trailing ? (
<Trailing className={styles.icon[size]} aria-hidden />
) : null}
</a>
);
}import type {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
ComponentType,
ReactNode,
Ref,
} from "react";
import { cx, sortCx } from "@/utils/cx";
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
type ButtonSize = "medium" | "small" | "xs";
type IconComponent = ComponentType<{
className?: string;
"aria-hidden"?: boolean | "true" | "false";
}>;
export interface ButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
variant?: ButtonVariant;
size?: ButtonSize;
iconOnly?: boolean;
leadingIcon?: IconComponent;
trailingIcon?: IconComponent;
children?: ReactNode;
ref?: Ref<HTMLButtonElement>;
}
export interface ButtonLinkProps
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "children"> {
variant?: ButtonVariant;
size?: ButtonSize;
iconOnly?: boolean;
leadingIcon?: IconComponent;
trailingIcon?: IconComponent;
children?: ReactNode;
ref?: Ref<HTMLAnchorElement>;
}
const styles = sortCx({
base: [
"inline-flex items-center justify-center gap-0.5 whitespace-nowrap overflow-hidden",
"font-sans select-none cursor-pointer",
"button-press-motion",
"outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-focus-ring",
"disabled:cursor-not-allowed aria-disabled:cursor-not-allowed",
].join(" "),
// Base shape per size (used when label is present OR medium icon-only).
size: {
medium: "h-9 rounded-2lg p-2 text-body-medium",
small: "h-8 rounded-lg px-2 py-1.5 text-body-medium",
xs: "h-6 rounded-sm px-2 text-caption-1-semibold",
},
// Icon-only override:
// Medium → keep p-2; w expands from content (8+20+8 = 36) → square.
// and zero the padding; the inner flex centers the 18px icon.
// Xs → forces 24×24, content-centered — used for the calendar
// template's edit-icon buttons (timezone/participants/reminder).
iconOnlySize: {
medium: "", // base size already produces 36×36 with a 20px icon
small: "size-8 p-0", // hard 32×32, content-centered
xs: "size-6 p-0", // hard 24×24, content-centered
},
icon: {
medium: "size-5 shrink-0", // 20px
small: "size-[18px] shrink-0", // 18px
xs: "size-3.5 shrink-0", // 14px
},
label: {
medium: "inline-flex items-center justify-center px-1 shrink-0", // px=4
small: "inline-flex items-center justify-center px-0.5 shrink-0", // px=2
xs: "inline-flex items-center justify-center px-0.5 shrink-0", // px=2
},
variant: {
primary: [
"bg-button-primary text-text-white shadow-xs",
"disabled:text-button-primary-disabled-foreground disabled:shadow-none",
"aria-disabled:text-button-primary-disabled-foreground aria-disabled:shadow-none",
].join(" "),
danger: [
"bg-button-danger text-text-white shadow-xs",
"disabled:text-foreground-disabled-danger disabled:shadow-none",
"aria-disabled:text-foreground-disabled-danger aria-disabled:shadow-none",
].join(" "),
secondary: [
"bg-background-primary-default text-text-primary",
"border border-border-button-default shadow-xs",
"hover:bg-background-primary-hover hover:border-border-button-hover",
"active:bg-background-primary-active active:border-border-button-active",
"disabled:bg-background-primary-disabled disabled:border-border-button-default disabled:text-text-tertiary disabled:shadow-none",
"aria-disabled:bg-background-primary-disabled aria-disabled:border-border-button-default aria-disabled:text-text-tertiary aria-disabled:shadow-none",
].join(" "),
ghost: [
"bg-button-ghost-background text-button-ghost-foreground",
"hover:bg-button-ghost-hover active:bg-button-ghost-active",
"disabled:bg-button-ghost-disabled disabled:text-button-ghost-disabled-foreground disabled:shadow-none",
"aria-disabled:bg-button-ghost-disabled aria-disabled:text-button-ghost-disabled-foreground aria-disabled:shadow-none",
].join(" "),
},
});
export function Button({
variant = "primary",
size = "medium",
iconOnly = false,
leadingIcon: Leading,
trailingIcon: Trailing,
children,
className,
type = "button",
ref,
...props
}: ButtonProps) {
return (
<button
ref={ref}
type={type}
className={cx(
styles.base,
styles.size[size],
styles.variant[variant],
iconOnly && styles.iconOnlySize[size],
className,
)}
{...props}
>
{Leading ? <Leading className={styles.icon[size]} aria-hidden /> : null}
{!iconOnly && children !== undefined && children !== null && (
<span className={styles.label[size]}>{children}</span>
)}
{!iconOnly && Trailing ? (
<Trailing className={styles.icon[size]} aria-hidden />
) : null}
</button>
);
}
/** Anchor counterpart to Button for navigational actions. */
export function ButtonLink({
variant = "primary",
size = "medium",
iconOnly = false,
leadingIcon: Leading,
trailingIcon: Trailing,
children,
className,
ref,
...props
}: ButtonLinkProps) {
return (
<a
ref={ref}
className={cx(
styles.base,
styles.size[size],
styles.variant[variant],
iconOnly && styles.iconOnlySize[size],
className,
)}
{...props}
>
{Leading ? <Leading className={styles.icon[size]} aria-hidden /> : null}
{!iconOnly && children !== undefined && children !== null && (
<span className={styles.label[size]}>{children}</span>
)}
{!iconOnly && Trailing ? (
<Trailing className={styles.icon[size]} aria-hidden />
) : null}
</a>
);
}Props
Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.
Button
| Prop | Type | Default | Description |
|---|---|---|---|
| iconOnly | boolean | false | — |
| leadingIcon | IconComponent | — | — |
| size | "medium" | "small" | "xs" | medium | — |
| trailingIcon | IconComponent | — | — |
| variant | "primary" | "secondary" | "ghost" | "danger" | primary | — |
ButtonLink
Anchor counterpart to Button for navigational actions.
| Prop | Type | Default | Description |
|---|---|---|---|
| iconOnly | boolean | false | — |
| leadingIcon | IconComponent | — | — |
| size | "medium" | "small" | "xs" | medium | — |
| trailingIcon | IconComponent | — | — |
| variant | "primary" | "secondary" | "ghost" | "danger" | primary | — |