---
title: Button
description: Buttons for primary, secondary, destructive, and quiet actions.
group: Components
order: 19
---

Use one button for everything from primary actions to quiet icon controls. Change its emphasis and dimensions with `variant` and `size`.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <>
      <Button variant="outline">Button</Button>
      <Button variant="outline" size="icon" aria-label="Upload">
        <ArrowUpIcon />
      </Button>
    </>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add button
```

## Usage

```tsx
import { Button, ButtonGroup, buttonVariants } from "@/components/ui/button"
```

```tsx
<Button variant="outline">Button</Button>
```

```tsx
<Button variant="default | secondary | destructive | outline | ghost | link">
  Button
</Button>
```

## Size

Use `size` for text buttons. Dedicated icon-only sizes are covered below.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <>
      <Button size="xs">
        Extra Small
        <ArrowUpRightIcon data-icon="inline-end" />
      </Button>
      <Button size="sm">
        Small
        <ArrowUpRightIcon data-icon="inline-end" />
      </Button>
      <Button>
        Default
        <ArrowUpRightIcon data-icon="inline-end" />
      </Button>
      <Button size="lg">
        Large
        <ArrowUpRightIcon data-icon="inline-end" />
      </Button>
    </>
  )
}
```

## Default

A solid treatment for the primary action.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return <Button>Button</Button>
}
```

## Outline

A bordered treatment with a transparent fill and accent hover.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return <Button variant="outline">Outline</Button>
}
```

## Secondary

A softer fill for supporting actions.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return <Button variant="secondary">Secondary</Button>
}
```

## Ghost

No fill until hover. A good fit for toolbars and compact interfaces.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return <Button variant="ghost">Ghost</Button>
}
```

## Destructive

Use for deletion and other irreversible actions.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return <Button variant="destructive">Destructive</Button>
}
```

## Link

An inline text treatment with an underline on hover.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return <Button variant="link">Link</Button>
}
```

## Icon

Use the square sizes for icon-only controls, and always provide an `aria-label`.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <>
      <Button size="icon-xs" aria-label="Update">
        <CircleFadingArrowUpIcon />
      </Button>
      <Button size="icon-sm" aria-label="Update">
        <CircleFadingArrowUpIcon />
      </Button>
      <Button size="icon" aria-label="Update">
        <CircleFadingArrowUpIcon />
      </Button>
      <Button size="icon-lg" aria-label="Update">
        <CircleFadingArrowUpIcon />
      </Button>
    </>
  )
}
```

## With icon

Place an SVG inside the button. Add `data-icon="inline-start"` or `data-icon="inline-end"` to tighten spacing on that side.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <>
      <Button variant="outline">
        <GitBranchIcon data-icon="inline-start" />
        New Branch
      </Button>
      <Button variant="outline">
        <GitForkIcon data-icon="inline-start" />
        Fork
      </Button>
    </>
  )
}
```

## Rounded

Add `!rounded-full` for a pill shape. It overrides the default `rounded-md`.

```tsx
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <>
      <Button className="!rounded-full">Get Started</Button>
      <Button className="!rounded-full" size="icon" aria-label="Upload">
        <ArrowUpIcon />
      </Button>
    </>
  )
}
```

## Spinner

Set `loading` to replace the label with a centered spinner. The button keeps its width and remains disabled while loading.

```tsx
import { useState } from "react"
import { Button } from "@/components/ui/button"

export function Example() {
  const [loading, setLoading] = useState(false)

  return (
    <Button
      loading={loading}
      onClick={() => {
        setLoading(true)
        window.setTimeout(() => setLoading(false), 1600)
      }}
    >
      Submit
    </Button>
  )
}
```

## Button group

Wrap related buttons in `ButtonGroup` to join their edges into one compact control.

```tsx
import {
  Button,
  ButtonGroup,
  buttonVariants,
} from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function Example() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Button variant="outline" size="icon" aria-label="Back">
        <ArrowLeftIcon />
      </Button>
      <ButtonGroup>
        <Button variant="outline">Archive</Button>
        <Button variant="outline">Report</Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">Snooze</Button>
        <DropdownMenu>
          <DropdownMenuTrigger
            aria-label="More actions"
            className={buttonVariants({ variant: "outline", size: "icon" })}
          >
            <MoreHorizontalIcon />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuItem>
              <CheckIcon />
              Mark as done
            </DropdownMenuItem>
            <DropdownMenuItem>
              <ArchiveIcon />
              Move to archive
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <ClockIcon />
              Remind me later
            </DropdownMenuItem>
            <DropdownMenuItem>
              <PinIcon />
              Pin this scene
            </DropdownMenuItem>
            <DropdownMenuItem>
              <ShareIcon />
              Copy share link
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </ButtonGroup>
    </div>
  )
}
```

```tsx
import { Button, ButtonGroup } from "@/components/ui/button"

<ButtonGroup>
  <Button variant="outline">Archive</Button>
  <Button variant="outline">Report</Button>
</ButtonGroup>
```

## As link

Use `buttonVariants` on an anchor when the action navigates. This keeps the correct link semantics while matching the button styles.

```tsx
import { buttonVariants } from "@/components/ui/button"

export function Example() {
  return (
    <a href="#login" className={buttonVariants()}>
      Login
    </a>
  )
}
```

## API Reference

### Button

| Prop | Type | Default |
| --- | --- | --- |
| variant | "default" \| "secondary" \| "destructive" \| "outline" \| "ghost" \| "link" | "default" |
| size | "default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" | "default" |
| loading | boolean | false |
| className | string | — |

```tsx
<Button variant="outline">Save</Button>
```

### buttonVariants

Returns the same classes as `Button` for links and other elements.

| Prop | Type | Default |
| --- | --- | --- |
| variant | "default" \| "secondary" \| "destructive" \| "outline" \| "ghost" \| "link" | "default" |
| size | "default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" | "default" |
| className | string | — |

```tsx
<a className={buttonVariants({ variant: "outline" })} href="/docs">
  Docs
</a>
```

### ButtonGroup

Also exported from `@arctis-sh/ui/button`. See [Button Group](/docs/components/button-group).

| Prop | Type | Default |
| --- | --- | --- |
| orientation | "horizontal" \| "vertical" | "horizontal" |
| className | string | — |

```tsx
<ButtonGroup>
  <Button variant="outline">Left</Button>
  <Button variant="outline">Right</Button>
</ButtonGroup>
```

## Source

```tsx
"use client";

import {
  Children,
  type ButtonHTMLAttributes,
  type ReactNode,
} from "react";
import {
  buttonGroupItemClass,
  buttonGroupOutlineJoinClass,
  useButtonGroup,
} from "@/components/ui/button-group";
import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";

type ButtonVariant =
  | "default"
  | "secondary"
  | "destructive"
  | "outline"
  | "ghost"
  | "link";

type ButtonSize =
  | "default"
  | "xs"
  | "sm"
  | "lg"
  | "icon"
  | "icon-xs"
  | "icon-sm"
  | "icon-lg";

type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
  children?: ReactNode;
};

const variantClass: Record<ButtonVariant, string> = {
  default: "bg-primary text-primary-foreground hover:opacity-85",
  secondary: "bg-secondary text-secondary-foreground hover:opacity-85",
  destructive: "bg-destructive text-destructive-foreground hover:opacity-85",
  outline:
    "border border-border bg-transparent text-foreground hover:bg-muted hover:text-accent-foreground dark:hover:bg-accent",
  ghost:
    "bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
  link: "group/button-link rounded-none bg-transparent text-foreground",
};

const sizeClass: Record<ButtonSize, string> = {
  default: "h-9 gap-2 px-4 text-sm has-[[data-icon]]:px-3",
  xs: "h-6 gap-1 rounded-md px-2 text-xs has-[[data-icon]]:px-1.5 [&_svg]:size-3",
  sm: "h-8 gap-1.5 px-3 text-sm has-[[data-icon]]:px-2.5",
  lg: "h-10 gap-2 px-6 text-sm has-[[data-icon]]:px-4",
  icon: "size-9",
  "icon-xs": "size-6 [&_svg]:size-3",
  "icon-sm": "size-8",
  "icon-lg": "size-10",
};

function linkLabel(children: ReactNode) {
  return Children.map(children, (child) => {
    if (typeof child !== "string" && typeof child !== "number") {
      return child;
    }

    return (
      <span
        data-slot="button-link-label"
        className="relative inline after:absolute after:inset-x-0 after:bottom-0 after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-200 after:ease-out group-hover/button-link:after:scale-x-100"
      >
        {child}
      </span>
    );
  });
}

export function buttonVariants({
  variant = "default",
  size = "default",
  className,
}: {
  variant?: ButtonVariant;
  size?: ButtonSize;
  className?: string;
} = {}) {
  return cn(
    "inline-flex shrink-0 items-center justify-center rounded-md font-normal tracking-wide whitespace-nowrap transition-all duration-200 ease-out disabled:pointer-events-none disabled:opacity-40",
    "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
    "has-[[data-icon=inline-start]]:pl-2.5 has-[[data-icon=inline-end]]:pr-2.5",
    variantClass[variant],
    sizeClass[size],
    variant === "link" && "h-auto w-fit !px-0",
    className,
  );
}

export function Button({
  variant = "default",
  size = "default",
  loading = false,
  className,
  type = "button",
  disabled,
  children,
  ...props
}: ButtonProps) {
  const group = useButtonGroup();
  const isDisabled = disabled || loading;
  const content = variant === "link" ? linkLabel(children) : children;

  return (
    <button
      type={type}
      data-slot="button"
      data-variant={variant}
      data-size={size}
      data-loading={loading ? "" : undefined}
      aria-busy={loading || undefined}
      disabled={isDisabled}
      className={buttonVariants({
        variant,
        size,
        className: cn(
          loading && "relative pointer-events-none !opacity-100",
          buttonGroupItemClass(group),
          variant === "outline" && buttonGroupOutlineJoinClass(group),
          className,
        ),
      })}
      {...props}
    >
      <span
        className={cn(
          "inline-flex items-center justify-center",
          size === "xs" || size === "icon-xs"
            ? "gap-1"
            : size === "sm"
              ? "gap-1.5"
              : "gap-2",
          loading && "invisible",
        )}
      >
        {content}
      </span>
      {loading ? (
        <span className="absolute inset-0 inline-flex items-center justify-center">
          <Spinner
            aria-hidden="true"
            className={cn(
              (size === "xs" || size === "icon-xs") && "size-3",
            )}
          />
        </span>
      ) : null}
    </button>
  );
}

export {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from "@/components/ui/button-group";
```
