# Button

A polymorphic button with six variants, eleven color schemes, icon slots and built-in loading states.

```tsx
"use client";

import { Button, Icon, HStack } from "astralis-ui";
import { Sparkles } from "lucide-react";

export function ButtonDemo() {
  return (
    <HStack gap="3" wrap="wrap" justifyContent="center">
      <Button leftIcon={<Icon as={Sparkles} size="xs" />}>Get started</Button>
      <Button variant="subtle">Learn more</Button>
      <Button variant="outline" colorScheme="gray">
        Cancel
      </Button>
    </HStack>
  );
}
```

## Import

```tsx
```

## Usage

The default button is `solid` in the brand color at size `md`. Every visual
decision — variant, hue, size, radius — is one prop away.

```tsx
import { Button, HStack } from "astralis-ui";

export function ButtonVariants() {
  return (
    <HStack gap="3" wrap="wrap" justifyContent="center">
      <Button variant="solid">Solid</Button>
      <Button variant="subtle">Subtle</Button>
      <Button variant="surface">Surface</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="text">Text</Button>
      <Button variant="link">Link</Button>
    </HStack>
  );
}
```

- **solid** — the signature fill. Use it for the single primary action on a screen.
- **subtle** — a tinted fill with no border, for secondary actions.
- **surface** — the bordered sibling of `subtle`.
- **outline** — border only; fills with a faint tint on hover.
- **text** — no chrome until hover (a ghost button).
- **link** — an inline-link affordance with an underline on hover.

## Color schemes

Every variant paints through the accent channel, so a single `colorScheme`
prop recolors any button — including [runtime brand colors](/docs/theming).
Use `gray` for neutral actions and `red` for destructive ones. The full
matrix below shows all six variants across all eleven schemes.

```tsx
import { Button, Text, VStack, HStack } from "astralis-ui";

const variants = ["solid", "subtle", "surface", "outline", "text", "link"] as const;
const schemes = [
  "brand", "gray", "red", "orange", "yellow", "green",
  "teal", "blue", "cyan", "purple", "pink",
] as const;

export function ButtonColorSchemes() {
  return (
    <VStack gap="6" alignItems="stretch">
      {variants.map((variant) => (
        <VStack key={variant} gap="2" alignItems="stretch">
          <Text as="span" size="xs" weight="medium" color="muted">
            {variant}
          </Text>
          <HStack gap="2" wrap="wrap">
            {schemes.map((scheme) => (
              <Button key={scheme} size="xs" variant={variant} colorScheme={scheme}>
                {scheme}
              </Button>
            ))}
          </HStack>
        </VStack>
      ))}
    </VStack>
  );
}
```

## Sizes

Five sizes, from compact toolbars to hero calls-to-action. Height, padding,
font size and icon gap scale together.

```tsx
import { Button, HStack } from "astralis-ui";

export function ButtonSizes() {
  return (
    <HStack gap="3" wrap="wrap" justifyContent="center">
      <Button size="xs">Extra small</Button>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
      <Button size="xl">Extra large</Button>
    </HStack>
  );
}
```

## Rounded

The corner radius is independent of size — go from sharp to pill with the
`rounded` prop.

```tsx
import { Button, HStack } from "astralis-ui";

export function ButtonRounded() {
  return (
    <HStack gap="3" wrap="wrap" justifyContent="center">
      <Button variant="surface" rounded="none">None</Button>
      <Button variant="surface" rounded="sm">Small</Button>
      <Button variant="surface" rounded="lg">Large</Button>
      <Button variant="surface" rounded="2xl">2XL</Button>
      <Button variant="surface" rounded="full">Full</Button>
    </HStack>
  );
}
```

## Icons

Pass any element to `leftIcon` or `rightIcon`. Omit the label and the button
becomes a perfect square — icon-only buttons **must** carry an `aria-label`
(the library warns in development if one is missing).

```tsx
"use client";

import { Button, Icon, HStack } from "astralis-ui";
import { Search, ChevronDown, Settings, Trash2 } from "lucide-react";

export function ButtonIcons() {
  return (
    <HStack gap="3" wrap="wrap" justifyContent="center">
      <Button leftIcon={<Icon as={Search} size="xs" />}>Search</Button>
      <Button variant="outline" rightIcon={<Icon as={ChevronDown} size="xs" />}>
        Open menu
      </Button>
      {/* Icon-only buttons need an aria-label for assistive tech. */}
      <Button variant="subtle" leftIcon={<Icon as={Settings} size="xs" />} aria-label="Settings" />
      <Button
        variant="subtle"
        colorScheme="red"
        leftIcon={<Icon as={Trash2} size="xs" />}
        aria-label="Delete"
      />
    </HStack>
  );
}
```

## Loading state

`loading` swaps in a spinner and blocks interaction without a layout shift.
`loadingText` replaces the label while pending, and `loaderPlacement` moves
the spinner to either side. Bring your own spinner via `loader`.

```tsx
"use client";

import { useState } from "react";
import { Button, HStack } from "astralis-ui";

export function ButtonLoading() {
  const [saving, setSaving] = useState(false);

  const save = () => {
    setSaving(true);
    setTimeout(() => setSaving(false), 2000);
  };

  return (
    <HStack gap="3" wrap="wrap" justifyContent="center">
      <Button loading>Loading</Button>
      <Button variant="outline" loading loadingText="Submitting…" />
      <Button variant="subtle" loading loaderPlacement="end" loadingText="Uploading" />
      <Button onClick={save} loading={saving} loadingText="Saving…">
        Click to save
      </Button>
    </HStack>
  );
}
```

## Full width

`fullWidth` stretches the button to its container — typical for checkout
flows and dialog footers.

```tsx
"use client";

import { Button, Icon, VStack } from "astralis-ui";
import { CreditCard } from "lucide-react";

export function ButtonFullWidth() {
  return (
    <VStack gap="3" w="full" maxW="sm">
      <Button fullWidth leftIcon={<Icon as={CreditCard} size="xs" />}>
        Complete checkout
      </Button>
      <Button fullWidth variant="outline" colorScheme="gray">
        Continue shopping
      </Button>
    </VStack>
  );
}
```

## Rendering as a link

Button is polymorphic: pass `as` to change the underlying element and the
accepted props follow along type-safely. Disabled non-button elements get
`aria-disabled` and leave the tab order instead of the `disabled` attribute.

```tsx
"use client";

import { Button, Icon, HStack } from "astralis-ui";
import { ExternalLink } from "lucide-react";

export function ButtonAsLink() {
  return (
    <HStack gap="3" wrap="wrap" justifyContent="center">
      {/* Renders a real <a>; href/target come from the anchor's prop types. */}
      <Button
        as="a"
        href="https://github.com"
        target="_blank"
        variant="surface"
        rightIcon={<Icon as={ExternalLink} size="xs" />}
      >
        View on GitHub
      </Button>
      <Button as="a" href="#usage" variant="link">
        Jump to usage
      </Button>
    </HStack>
  );
}
```

Buttons compose with a surrounding [Button Group](/docs/components/button-group),
which supplies shared `variant`, `colorScheme`, `size` and `disabled` defaults —
an explicit prop on a child always wins.

## Props

Button also accepts every prop of the element it renders as (`button` by default).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | "solid" \| "subtle" \| "surface" \| "outline" \| "text" \| "link" | `"solid"` | Visual style. surface is the bordered sibling of subtle; text is the ghost style. |
| `colorScheme` | "brand" \| "gray" \| "red" \| "orange" \| "yellow" \| "green" \| "teal" \| "blue" \| "cyan" \| "purple" \| "pink" | `"brand"` | Hue the variant paints with. Use gray for a neutral button. |
| `size` | "xs" \| "sm" \| "md" \| "lg" \| "xl" | `"md"` | Height, padding, font size and icon gap scale together. |
| `rounded` | "none" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "full" | `"lg"` | Corner radius. |
| `fullWidth` | boolean | `false` | Stretches the button to fill its container. |
| `disabled` | boolean | `false` | Disables the button. On non-button elements this maps to aria-disabled and removes it from the tab order. |
| `loading` | boolean | `false` | Shows a spinner and disables interaction. |
| `loadingText` | ReactNode | — | Replaces the label while loading (e.g. “Saving…”); the spinner still shows. |
| `loaderPlacement` | "start" \| "end" | `"start"` | Which side the spinner renders on. |
| `loader` | ReactNode | — | Custom spinner element, replacing the built-in one. |
| `leftIcon` | ReactNode | — | Icon rendered before the label. With no label the button becomes icon-only. |
| `rightIcon` | ReactNode | — | Icon rendered after the label. |
| `as` | ElementType | `"button"` | Element or component to render. Forwarded HTML props follow the chosen element automatically. |

## Accessibility

- Renders a native `<button>` by default, so keyboard and screen-reader
  semantics come for free.
- Icon-only buttons need an `aria-label`; a development-mode warning fires
  when it's missing.
- `disabled` and `loading` on non-button elements map to `aria-disabled` and
  `tabIndex={-1}` rather than the invalid `disabled` attribute.
- Focus is always visible: a two-pixel outline in the active color scheme's
  ring token, offset from the button edge.
