# Button (/ui/components/react/button)



<Playground component="button" />

## Usage [#usage]

```tsx
import { Button } from '@appica/ui-react/button'
```

```tsx
<Button>Click me</Button>
```

## Examples [#examples]

### Variants [#variants]

The `variant` prop sets the visual style. `light` is meant for dark or image backgrounds and is omitted below.

```tsx
import { Button } from '@appica/ui-react/button'

const variants = ['primary', 'primary-outline', 'secondary', 'soft', 'outline', 'ghost', 'destructive'] as const

const label = (variant: string) =>
  variant
    .split('-')
    .map((word) => word[0]!.toUpperCase() + word.slice(1))
    .join(' ')

export default function ButtonVariants() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {variants.map((variant) => (
        <Button key={variant} variant={variant}>
          {label(variant)}
        </Button>
      ))}
    </div>
  )
}
```

### Sizes [#sizes]

```tsx
import { Button } from '@appica/ui-react/button'

export default function ButtonSizes() {
  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>
  )
}
```

### With an icon [#with-an-icon]

Mark an icon with `data-icon="start"` or `data-icon="end"` so the button reserves the right inner spacing. Use an `icon-*` size for an icon-only button, and always give it an `aria-label`.

```tsx
import { Button } from '@appica/ui-react/button'
import { Download, ArrowUpRight, Bookmark } from '@appica/icons-react'

export default function ButtonWithIcon() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button variant="secondary">
        <Download data-icon="start" />
        Download
      </Button>
      <Button variant="outline">
        Open
        <ArrowUpRight data-icon="end" />
      </Button>
      <Button size="icon-md" variant="soft" aria-label="Bookmark">
        <Bookmark />
      </Button>
    </div>
  )
}
```

### Loading state [#loading-state]

While an action is in flight, set `disabled` to block further clicks and swap the label to a [`Spinner`](/ui/components/react/spinner). Add `focusableWhenDisabled` so focus stays on the button when it becomes disabled — without it, focus would jump to the body. Give the `Spinner` `currentColor` so it inherits the button's text color, and `data-icon="start"` so it gets the same inner spacing as an icon.

```tsx
'use client'

import * as React from 'react'
import { Button } from '@appica/ui-react/button'
import { Spinner } from '@appica/ui-react/spinner'

export default function ButtonLoading() {
  const [loading, setLoading] = React.useState(false)

  return (
    <Button
      disabled={loading}
      focusableWhenDisabled
      onClick={() => {
        setLoading(true)
        setTimeout(() => setLoading(false), 3000)
      }}
    >
      {loading && <Spinner variant="dots" currentColor data-icon="start" className="text-[1.125rem]" />}
      {loading ? 'Submitting' : 'Submit'}
    </Button>
  )
}
```

### As a link [#as-a-link]

A link should stay a link. To make an `<a>` (or your router's `<Link>`) merely *look* like a button, apply the exported `buttonVariants` classes to it directly — don't `render` it through `Button`. See [Composition → Links that look like a button](/ui/docs/react/composition#links-that-look-like-a-button).

```tsx
import { buttonVariants } from '@appica/ui-react/button'
import { ArrowUpRight } from '@appica/icons-react'

export default function ButtonAsLink() {
  return (
    <a
      href="https://base-ui.com"
      target="_blank"
      rel="noreferrer"
      className={buttonVariants({ variant: 'soft', size: 'md' })}
    >
      Visit Base UI
      <ArrowUpRight data-icon="end" />
    </a>
  )
}
```

<Callout type="warning" title="Don't render a link through Button">
  Rendering an `<a>` through `Button` forces button semantics onto a link, which misleads assistive tech —
  use `buttonVariants` on the link instead. The `render` prop with `nativeButton={false}` is only for
  rendering as a *non-interactive* element (a `<div>` / `<span>`) where you still want button behavior.
</Callout>

```tsx
// render is for adopting a different element while KEEPING button semantics — e.g. a <div>:
<Button render={<div />} nativeButton={false}>
  Not a native button
</Button>
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, popup placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="button" />

## API reference [#api-reference]

`Button` wraps [Base UI's Button](https://base-ui.com/react/components/button) and passes every remaining prop (`onClick`, `type`, `aria-*`, `ref`, …) straight through, so anything the Base UI primitive accepts works here — see its docs for the full API. Inside a [`ButtonGroup`](/ui/components/react/button-group), `variant` and `size` are inherited from the group unless set on the button.

| Prop           | <ColMinWidth width="200">Type</ColMinWidth>                                                                              | Default     | <ColMinWidth width="200">Description</ColMinWidth>                                                     |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------ |
| `variant`      | <Code>'primary' \| 'primary-outline' \| 'secondary' \| 'soft' \| 'outline' \| 'ghost' \| 'destructive' \| 'light'</Code> | `'primary'` | Visual style.                                                                                          |
| `size`         | <Code>'sm' \| 'md' \| 'lg' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'</Code>                                                 | `'md'`      | Height and padding. The `icon-*` sizes are square, for icon-only buttons.                              |
| `disabled`     | `boolean`                                                                                                                | `false`     | Disables interaction; sets `data-disabled` for styling.                                                |
| `render`       | <Code>ReactElement \| ((props, state) => ReactElement)</Code>                                                            | —           | Replace the button's HTML element with a different tag, or compose it with another component.          |
| `nativeButton` | `boolean`                                                                                                                | `true`      | Whether the rendered element is a native `<button>`. Set `false` when `render`ing a non-button (link). |
| `className`    | `string`                                                                                                                 | —           | Extra classes, merged after the variant classes via `tailwind-merge`.                                  |

## Accessibility [#accessibility]

* Renders a native `<button>` (or whatever you pass to `render`), so it is focusable and in the tab order by default.
* **Enter** and **Space** activate the button.
* `disabled` removes the button from the tab order and sets `data-disabled` for styling; the pointer is disabled via `pointer-events-none`.
* Icon-only buttons have no text, so give them an `aria-label`.
* All press and hover transitions are wrapped in `motion-reduce:` variants, so they are skipped when the user prefers reduced motion.
