# Button

A pill-shaped button that triggers an action, with variants for every level of emphasis.

Button is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/components/button

**React**

**Example**

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

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

`Button` renders a native `button` by default. It supports eight visual variants, five text sizes and five icon-only sizes, and an optional `block` mode that fills the container width.

Use the `default` variant for the primary action in a view, `secondary`, `tinted` or `outline` for supporting actions, `ghost` and `plain` for low-emphasis actions, and `destructive` for actions that delete or remove. To join several buttons into one control, use Button Group.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/button
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add button
```

### Manual

1. Install the dependencies and add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper):

```bash title="Terminal"
npm install @base-ui/react class-variance-authority
```

2. Copy the source into `components/ui/button.tsx`:

```tsx title="components/ui/button.tsx"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import {
  type CxOptions,
  cva,
  cx,
  type VariantProps,
} from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonStyles = cva(
  "inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent bg-clip-padding text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground hover:bg-primary/80 disabled:opacity-50",
        outline:
          "border-border bg-background hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent aria-expanded:text-accent-foreground dark:bg-transparent dark:hover:bg-accent dark:aria-expanded:bg-accent dark:hover:text-accent-foreground disabled:opacity-50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50",
        tinted:
          "bg-primary/10 dark:bg-primary/20 text-primary hover:bg-primary/20 dark:hover:bg-primary/30 disabled:opacity-50",
        ghost:
          "hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent aria-expanded:text-accent-foreground disabled:opacity-50",
        plain:
          "text-primary hover:bg-accent aria-expanded:bg-accent disabled:opacity-50",
        destructive:
          "bg-destructive/10 dark:bg-destructive/20 text-destructive hover:bg-destructive/20 dark:hover:bg-destructive/30 hover:text-[color-mix(in_oklab,var(--destructive),var(--foreground)_15%)] focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:text-destructive disabled:opacity-50",
        link: "text-primary hover:underline disabled:opacity-50",
      },
      size: {
        default:
          "h-9 gap-1.5 px-4 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3",
        xs: "h-6 gap-1 px-2.75 text-xs has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        lg: "h-11 gap-1.5 px-5.5 text-base has-data-[icon=inline-end]:pe-4.5 has-data-[icon=inline-start]:ps-4.5",
        xl: "h-14 gap-2 px-7.75 text-base has-data-[icon=inline-end]:pe-6 has-data-[icon=inline-start]:ps-6",
        icon: "size-9",
        "icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
        "icon-sm": "size-8",
        "icon-lg": "size-11",
        "icon-xl": "size-14",
      },
      block: {
        true: "w-full",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

const blockRadius = cva("", {
  variants: {
    size: {
      default: "rounded-md",
      xs: "rounded-sm",
      sm: "rounded-md",
      lg: "rounded-lg",
      xl: "rounded-xl",
      icon: "",
      "icon-xs": "",
      "icon-sm": "",
      "icon-lg": "",
      "icon-xl": "",
    },
  },
  defaultVariants: {
    size: "default",
  },
})

function buttonVariants({
  block,
  size,
  className,
  ...props
}: VariantProps<typeof buttonStyles> & { className?: CxOptions[number] } = {}) {
  return cx(
    buttonStyles({ ...props, size, block }),
    block ? blockRadius({ size }) : undefined,
    className
  )
}

function Button({
  className,
  variant,
  size,
  block,
  ...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
  return (
    <ButtonPrimitive
      data-slot="button"
      data-variant={variant ?? "default"}
      data-size={size ?? "default"}
      className={cn(buttonVariants({ variant, size, block, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }
```

3. Update the import paths to match your project.

## Usage

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

```tsx
<Button>Button</Button>
```

Mark icons with `data-icon="inline-start"` or `data-icon="inline-end"` to tighten the padding on that side. Icons without a size class are sized automatically.

Use an `icon` size, such as `icon` or `icon-sm`, for buttons that contain only an icon, and give them an `aria-label`.

To render a link or a router component with button styling, use the `render` prop from Base UI. `buttonVariants` is also exported if you need the classes on another element.

## Examples

### Variants

Shows all eight variants side by side.

**Example**

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

export default function ButtonVariants() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button variant="default">Default</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="tinted">Tinted</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="plain">Plain</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="link">Link</Button>
    </div>
  )
}
```

### Sizes

Compares the `xs`, `sm`, `default` and `lg` sizes.

**Example**

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

export default function ButtonSizes() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button size="xs">Extra small</Button>
      <Button size="sm">Small</Button>
      <Button size="default">Default</Button>
      <Button size="lg">Large</Button>
    </div>
  )
}
```

### With icon

Adds leading and trailing icons, and an icon-only button.

**Example**

```tsx
import { ArrowRightIcon, PlusIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"

export default function ButtonWithIcon() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button>
        <PlusIcon data-icon="inline-start" />
        New project
      </Button>
      <Button variant="outline">
        Continue
        <ArrowRightIcon data-icon="inline-end" />
      </Button>
      <Button size="icon" aria-label="Add">
        <PlusIcon />
      </Button>
    </div>
  )
}
```

### Disabled

A disabled button that is skipped in the tab order.

**Example**

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

export default function ButtonDisabled() {
  return <Button disabled>Button</Button>
}
```

### Block

Full-width buttons at each size, whose corner radius scales with size.

**Example**

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

export default function ButtonBlock() {
  return (
    <div className="flex w-80 flex-col gap-3">
      <Button block size="xs">
        Add to Bag
      </Button>
      <Button block size="sm">
        Add to Bag
      </Button>
      <Button block>Add to Bag</Button>
      <Button block size="lg">
        Add to Bag
      </Button>
      <Button block size="xl">
        Add to Bag
      </Button>
      <Button block variant="secondary">
        Continue
      </Button>
    </div>
  )
}
```

## API Reference

### Button

The interactive button element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline" \| "secondary" \| "tinted" \| "ghost" \| "plain" \| "destructive" \| "link"` | `"default"` | Visual style: `default`, `outline`, `secondary`, `tinted`, `ghost`, `plain`, `destructive` or `link`. |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "xl" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| "icon-xl"` | `"default"` | Height and padding. `xs` to `xl` for text buttons; `icon-xs` to `icon-xl` for square icon-only buttons. |
| `block` | `true` | - | When `true`, the button fills the container width and uses rounded corners instead of a pill shape. |

Also accepts the props of the primitive part it wraps.

## Accessibility

- Renders a native `button`, so `Enter` and `Space` activate it.
- A disabled button is removed from the tab order.
- Icon-only buttons need an `aria-label`.
- Keyboard focus shows a 3px ring in `--ring`; the `destructive` variant tints it with `ring-destructive/20`.
- `aria-invalid` adds a destructive border and ring, and `aria-expanded` shows a selected background on `outline`, `ghost` and `plain` buttons that open menus or popovers.

## Notes

- Styling hook: `data-slot="button"`.
- Colors come from the `primary`, `secondary`, `accent` and `destructive` variables. Hover uses the same color with an opacity, such as `hover:bg-primary/80`, and disabled buttons use `opacity-50`.
- Buttons in a group raise the focused one above its neighbors so the whole ring stays visible.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/components-button--default.light.webp)

```tsx
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"

export function ButtonExample() {
  return (
    <View className="items-start">
      <Button>
        <Text>Button</Text>
      </Button>
    </View>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct ButtonExample: View {
  var body: some View {
    Button("Button") {}
      .buttonStyle(.variant())
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.Button

@Composable
fun ButtonDefault() {
    Column {
        Button("Button", onClick = {})
    }
}
```

`Button` is a `Pressable` with the `button` role. It supports eight visual variants, five text sizes and five icon-only sizes, and an optional `block` mode that fills the width of its container.

Use the `default` variant for the primary action on a screen, `secondary`, `tinted` or `outline` for supporting actions, `ghost` and `plain` for low-emphasis actions, and `destructive` for actions that delete or remove. To join several buttons into one control, use Button Group.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/button
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add button
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add button --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add button --platform kotlin
```

The CLI also adds what it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install class-variance-authority cn
```

2. Add the pieces it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/ui/button.tsx`:

```tsx title="uniwind/components/ui/button.tsx"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import { Pressable } from "react-native"
import { IconClassContext } from "@/components/ui/icon"
import { TextClassContext } from "@/components/ui/text"

const buttonVariants = cva(
  "shrink-0 flex-row items-center justify-center rounded-full border border-transparent",
  {
    variants: {
      variant: {
        default: "bg-primary active:opacity-80 disabled:opacity-50",
        outline:
          "border-border bg-background active:opacity-80 disabled:opacity-50 dark:bg-transparent",
        secondary: "bg-secondary active:opacity-80 disabled:opacity-50",
        tinted:
          "bg-primary/10 active:opacity-80 disabled:opacity-50 dark:bg-primary/20",
        ghost: "active:opacity-80 disabled:opacity-50",
        plain: "active:opacity-80 disabled:opacity-50",
        destructive:
          "bg-destructive/10 active:opacity-80 disabled:opacity-50 dark:bg-destructive/20",
        link: "active:opacity-80 disabled:opacity-50",
      },
      size: {
        default: "h-9 gap-1.5 px-4",
        xs: "h-6 gap-1 px-2.75",
        sm: "h-8 gap-1 px-3",
        lg: "h-11 gap-1.5 px-5.5",
        xl: "h-14 gap-2 px-7.75",
        icon: "size-9",
        "icon-xs": "size-6",
        "icon-sm": "size-8",
        "icon-lg": "size-11",
        "icon-xl": "size-14",
      },
      block: {
        true: "w-full",
      },
    },
    compoundVariants: [
      { block: true, size: "xs", className: "rounded-sm" },
      { block: true, size: ["sm", "default"], className: "rounded-md" },
      { block: true, size: "lg", className: "rounded-lg" },
      { block: true, size: "xl", className: "rounded-xl" },
    ],
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

const buttonTextVariants = cva("text-sm text-foreground", {
  variants: {
    variant: {
      default: "text-primary-foreground",
      outline: "",
      secondary: "text-secondary-foreground",
      tinted: "text-primary",
      ghost: "",
      plain: "text-primary",
      destructive: "text-destructive",
      link: "text-primary",
    },
    size: {
      default: "",
      xs: "text-xs",
      sm: "",
      lg: "text-base",
      xl: "text-base",
      icon: "",
      "icon-xs": "",
      "icon-sm": "",
      "icon-lg": "",
      "icon-xl": "",
    },
  },
  defaultVariants: {
    variant: "default",
    size: "default",
  },
})

type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>

const ButtonClassContext = React.createContext<
  ((variant: ButtonVariant) => string | undefined) | undefined
>(undefined)

type ButtonProps = React.ComponentProps<typeof Pressable> &
  React.RefAttributes<React.ComponentRef<typeof Pressable>> &
  VariantProps<typeof buttonVariants>

function Button({ className, variant, size, block, ...props }: ButtonProps) {
  const groupClassName = React.useContext(ButtonClassContext)
  return (
    <ButtonClassContext.Provider value={undefined}>
      <TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
        <IconClassContext.Provider
          value={size === "xs" || size === "icon-xs" ? "size-3" : "size-4"}
        >
          <Pressable
            className={cn(
              buttonVariants({ variant, size, block }),
              groupClassName?.(variant ?? "default"),
              className
            )}
            role="button"
            {...props}
          />
        </IconClassContext.Provider>
      </TextClassContext.Provider>
    </ButtonClassContext.Provider>
  )
}

export type { ButtonProps, ButtonVariant }
export { Button, ButtonClassContext, buttonTextVariants, buttonVariants }
```

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install class-variance-authority cn
```

2. Add the pieces it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/ui/button.tsx`:

```tsx title="nativewind/components/ui/button.tsx"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import { Pressable } from "react-native"
import { IconClassContext } from "@/components/ui/icon"
import { TextClassContext } from "@/components/ui/text"

const buttonVariants = cva(
  "shrink-0 flex-row items-center justify-center rounded-full border border-transparent",
  {
    variants: {
      variant: {
        default: "bg-primary active:opacity-80 disabled:opacity-50",
        outline:
          "border-border bg-background active:opacity-80 disabled:opacity-50 dark:bg-transparent",
        secondary: "bg-secondary active:opacity-80 disabled:opacity-50",
        tinted:
          "bg-primary-10 active:opacity-80 disabled:opacity-50 dark:bg-primary-20",
        ghost: "active:opacity-80 disabled:opacity-50",
        plain: "active:opacity-80 disabled:opacity-50",
        destructive:
          "bg-destructive-10 active:opacity-80 disabled:opacity-50 dark:bg-destructive-20",
        link: "active:opacity-80 disabled:opacity-50",
      },
      size: {
        default: "h-9 gap-1.5 px-4",
        xs: "h-6 gap-1 px-[0.6875rem]",
        sm: "h-8 gap-1 px-3",
        lg: "h-11 gap-1.5 px-[1.375rem]",
        xl: "h-14 gap-2 px-[1.9375rem]",
        icon: "size-9",
        "icon-xs": "size-6",
        "icon-sm": "size-8",
        "icon-lg": "size-11",
        "icon-xl": "size-14",
      },
      block: {
        true: "w-full",
      },
    },
    compoundVariants: [
      { block: true, size: "xs", className: "rounded-sm" },
      { block: true, size: ["sm", "default"], className: "rounded-md" },
      { block: true, size: "lg", className: "rounded-lg" },
      { block: true, size: "xl", className: "rounded-xl" },
    ],
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

const buttonTextVariants = cva("text-sm text-foreground", {
  variants: {
    variant: {
      default: "text-primary-foreground",
      outline: "",
      secondary: "text-secondary-foreground",
      tinted: "text-primary",
      ghost: "",
      plain: "text-primary",
      destructive: "text-destructive",
      link: "text-primary",
    },
    size: {
      default: "",
      xs: "text-xs",
      sm: "",
      lg: "text-base",
      xl: "text-base",
      icon: "",
      "icon-xs": "",
      "icon-sm": "",
      "icon-lg": "",
      "icon-xl": "",
    },
  },
  defaultVariants: {
    variant: "default",
    size: "default",
  },
})

type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>

const ButtonClassContext = React.createContext<
  ((variant: ButtonVariant) => string | undefined) | undefined
>(undefined)

type ButtonProps = React.ComponentProps<typeof Pressable> &
  React.RefAttributes<React.ComponentRef<typeof Pressable>> &
  VariantProps<typeof buttonVariants>

function Button({ className, variant, size, block, ...props }: ButtonProps) {
  const groupClassName = React.useContext(ButtonClassContext)
  return (
    <ButtonClassContext.Provider value={undefined}>
      <TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
        <IconClassContext.Provider
          value={size === "xs" || size === "icon-xs" ? "size-3" : "size-4"}
        >
          <Pressable
            className={cn(
              buttonVariants({ variant, size, block }),
              groupClassName?.(variant ?? "default"),
              className
            )}
            role="button"
            {...props}
          />
        </IconClassContext.Provider>
      </TextClassContext.Provider>
    </ButtonClassContext.Provider>
  )
}

export type { ButtonProps, ButtonVariant }
export { Button, ButtonClassContext, buttonTextVariants, buttonVariants }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Add the pieces it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text).

2. Copy the source into `components/ui/button.tsx`:

```tsx title="stylesheet/components/ui/button.tsx"
import * as React from "react"
import {
  Pressable,
  type PressableStateCallbackType,
  type StyleProp,
  StyleSheet,
  type TextStyle,
  type ViewStyle,
} from "react-native"
import { IconSizeContext } from "@/components/ui/icon"
import { TextStyleContext } from "@/components/ui/text"
import {
  fontSize,
  radius,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

type ButtonVariant =
  | "default"
  | "outline"
  | "secondary"
  | "tinted"
  | "ghost"
  | "plain"
  | "destructive"
  | "link"

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

const sizes = StyleSheet.create({
  default: { height: 36, gap: 6, paddingHorizontal: 16 },
  xs: { height: 24, gap: 4, paddingHorizontal: 11 },
  sm: { height: 32, gap: 4, paddingHorizontal: 12 },
  lg: { height: 44, gap: 6, paddingHorizontal: 22 },
  xl: { height: 56, gap: 8, paddingHorizontal: 31 },
  icon: { width: 36, height: 36 },
  "icon-xs": { width: 24, height: 24 },
  "icon-sm": { width: 32, height: 32 },
  "icon-lg": { width: 44, height: 44 },
  "icon-xl": { width: 56, height: 56 },
})

const blockRadius: Partial<Record<ButtonSize, number>> = {
  xs: radius.sm,
  sm: radius.md,
  default: radius.md,
  lg: radius.lg,
  xl: radius.xl,
}

const styles = StyleSheet.create({
  root: {
    flexShrink: 0,
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.full,
    borderWidth: 1,
    borderColor: "transparent",
  },
  block: { width: "100%" },
})

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

function buttonColors(
  variant: ButtonVariant,
  colors: ThemeColors,
  isDark: boolean
): ViewStyle {
  switch (variant) {
    case "default":
      return { backgroundColor: colors.primary }
    case "outline":
      return {
        borderColor: colors.border,
        backgroundColor: isDark ? "transparent" : colors.background,
      }
    case "secondary":
      return { backgroundColor: colors.secondary }
    case "tinted":
      return { backgroundColor: withAlpha(colors.primary, isDark ? 0.2 : 0.1) }
    case "destructive":
      return {
        backgroundColor: withAlpha(colors.destructive, isDark ? 0.2 : 0.1),
      }
    default:
      return {}
  }
}

function buttonTextColor(variant: ButtonVariant, colors: ThemeColors) {
  switch (variant) {
    case "default":
      return colors.primaryForeground
    case "secondary":
      return colors.secondaryForeground
    case "tinted":
    case "plain":
    case "link":
      return colors.primary
    case "destructive":
      return colors.destructive
    default:
      return colors.foreground
  }
}

function buttonTextStyle(
  variant: ButtonVariant,
  size: ButtonSize,
  colors: ThemeColors
): TextStyle {
  let scale: TextStyle =
    size === "lg" || size === "xl" ? fontSize.base : fontSize.sm
  if (size === "xs") scale = fontSize.xs
  return { ...scale, color: buttonTextColor(variant, colors) }
}

const ButtonStyleContext = React.createContext<
  ((variant: ButtonVariant) => ViewStyle | undefined) | undefined
>(undefined)

type ButtonProps = Omit<React.ComponentProps<typeof Pressable>, "style"> &
  React.RefAttributes<React.ComponentRef<typeof Pressable>> & {
    variant?: ButtonVariant
    size?: ButtonSize
    block?: boolean
    style?:
      | StyleProp<ViewStyle>
      | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)
  }

function Button({
  variant = "default",
  size = "default",
  block = false,
  style,
  disabled,
  ...props
}: ButtonProps) {
  const { colors, isDark } = useTheme()
  const isDisabled = Boolean(disabled)
  const blockRadiusValue = block ? blockRadius[size] : undefined
  const groupStyle = React.useContext(ButtonStyleContext)
  return (
    <ButtonStyleContext.Provider value={undefined}>
      <TextStyleContext.Provider value={buttonTextStyle(variant, size, colors)}>
        <IconSizeContext.Provider
          value={size === "xs" || size === "icon-xs" ? 12 : 16}
        >
          <Pressable
            role="button"
            disabled={disabled}
            style={(state) => [
              styles.root,
              sizes[size],
              block && styles.block,
              blockRadiusValue !== undefined && {
                borderRadius: blockRadiusValue,
              },
              buttonColors(variant, colors, isDark),
              state.pressed && !isDisabled && { opacity: 0.8 },
              isDisabled && { opacity: 0.5 },
              groupStyle?.(variant),
              typeof style === "function" ? style(state) : style,
            ]}
            {...props}
          />
        </IconSizeContext.Provider>
      </TextStyleContext.Provider>
    </ButtonStyleContext.Provider>
  )
}

export type { ButtonProps, ButtonSize, ButtonVariant }
export { Button, ButtonStyleContext, buttonTextStyle }
```

3. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

3. Copy the source and every file it uses:

```swift title="swift/Components/Button.swift"
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
      case .xl, .iconXl: 56
      }
    }

    var horizontalPadding: CGFloat {
      switch self {
      case .xs: 11
      case .sm: 12
      case .default: 16
      case .lg: 22
      case .xl: 31
      case .icon, .iconXs, .iconSm, .iconLg, .iconXl: 0
      }
    }

    var spacing: CGFloat {
      switch self {
      case .xs, .sm, .iconXs, .iconSm: 4
      case .xl, .iconXl: 8
      default: 6
      }
    }

    var textStyle: TextStyle {
      switch self {
      case .xs, .iconXs: .xs
      case .lg, .xl: .base
      default: .sm
      }
    }

    public var iconSize: CGFloat {
      switch self {
      case .xs, .iconXs: 12
      default: 16
      }
    }

    var blockRadius: CGFloat {
      switch self {
      case .xs, .iconXs: Radius.sm
      case .lg, .iconLg: Radius.lg
      case .xl, .iconXl: Radius.xl
      default: Radius.md
      }
    }

    var isIcon: Bool {
      switch self {
      case .icon, .iconXs, .iconSm, .iconLg, .iconXl: true
      default: false
      }
    }
  }

  let variant: Variant
  let size: Size
  let block: Bool
  var metrics: ButtonMetrics?

  public init(_ variant: Variant = .default, size: Size = .default, block: Bool = false) {
    self.variant = variant
    self.size = size
    self.block = block
  }

  init(_ variant: Variant, size: Size, metrics: ButtonMetrics) {
    self.variant = variant
    self.size = size
    self.block = false
    self.metrics = metrics
  }

  public func makeBody(configuration: Configuration) -> some View {
    StyledButton(configuration: configuration, variant: variant, size: size, block: block, metrics: metrics)
  }
}

struct ButtonMetrics: Sendable {
  var horizontalPadding: CGFloat
  var spacing: CGFloat
  var iconSize: CGFloat
  var textStyle: TextStyle
}

private struct StyledButton: View {
  let configuration: ButtonStyleConfiguration
  let variant: VariantButtonStyle.Variant
  let size: VariantButtonStyle.Size
  let block: Bool
  let metrics: ButtonMetrics?

  @Environment(\.colorScheme) private var colorScheme
  @Environment(\.isEnabled) private var isEnabled
  @Environment(\.buttonGroup) private var group

  var body: some View {
    let joined = group != nil && variant != .ghost && variant != .plain
    let rounded = RoundedRectangle(
      cornerRadius: block ? size.blockRadius : Radius.full,
      style: .continuous
    )
    let shape = joined ? AnyShape(Rectangle()) : AnyShape(rounded)
    configuration.label
      .labelStyle(ButtonLabelStyle(spacing: metrics?.spacing ?? size.spacing, iconSize: metrics?.iconSize ?? size.iconSize))
    .scaledFont(metrics?.textStyle ?? size.textStyle)
    .underline(variant == .link && pressed && isEnabled)
    .lineLimit(1)
    .foregroundStyle(foreground)
    .padding(.horizontal, metrics?.horizontalPadding ?? size.horizontalPadding)
    .frame(width: size.isIcon && !block ? size.height : nil, height: size.height)
    .frame(maxWidth: block || group == .vertical ? .infinity : nil)
    .background(background, in: shape)
    .overlay {
      if variant == .outline && !joined {
        rounded.strokeBorder(EsuissColor.border)
      }
    }
    .contentShape(shape)
    .opacity(isEnabled ? (pressed ? 0.8 : 1) : 0.5)
    .anchorPreference(key: ButtonGroupFramesKey.self, value: .bounds) { anchor in
      joined ? [ButtonGroupFrame(bounds: anchor, outlined: variant == .outline)] : []
    }
  }

  private var isDark: Bool { colorScheme == .dark }
  private var pressed: Bool { configuration.isPressed }

  private var foreground: Color {
    switch variant {
    case .default:
      EsuissColor.primaryForeground
    case .outline, .ghost:
      EsuissColor.foreground
    case .secondary:
      EsuissColor.secondaryForeground
    case .tinted, .plain, .link:
      EsuissColor.primary
    case .destructive:
      EsuissColor.destructive
    }
  }

  private var background: Color {
    switch variant {
    case .default:
      EsuissColor.primary
    case .outline:
      isDark ? .clear : EsuissColor.background
    case .secondary:
      EsuissColor.secondary
    case .tinted:
      Color.adaptive(light: EsuissColor.primary.opacity(0.1), dark: EsuissColor.primary.opacity(0.2))
    case .ghost, .plain, .link:
      .clear
    case .destructive:
      Color.adaptive(light: EsuissColor.destructive.opacity(0.1), dark: EsuissColor.destructive.opacity(0.2))
    }
  }
}

struct ButtonLabelStyle: LabelStyle {
  let spacing: CGFloat
  let iconSize: CGFloat

  func makeBody(configuration: Configuration) -> some View {
    HStack(spacing: spacing) {
      configuration.icon
        .frame(width: iconSize, height: iconSize)
      configuration.title
    }
  }
}

extension ButtonStyle where Self == VariantButtonStyle {
  public static func variant(
    _ variant: VariantButtonStyle.Variant = .default,
    size: VariantButtonStyle.Size = .default,
    block: Bool = false
  ) -> VariantButtonStyle {
    VariantButtonStyle(variant, size: size, block: block)
  }
}
```

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

4. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

```kt title="kotlin/components/Button.kt"
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

enum class ButtonVariant { Default, Outline, Secondary, Tinted, Ghost, Plain, Destructive, Link }

enum class ButtonSize(
    val height: Dp,
    val horizontalPadding: Dp,
    val spacing: Dp,
    val iconSize: Dp,
    val blockRadius: Dp,
    val isIcon: Boolean,
) {
    Default(36.dp, 16.dp, 6.dp, 16.dp, Radius.md, false),
    Xs(24.dp, 11.dp, 4.dp, 12.dp, Radius.sm, false),
    Sm(32.dp, 12.dp, 4.dp, 16.dp, Radius.md, false),
    Lg(44.dp, 22.dp, 6.dp, 16.dp, Radius.lg, false),
    Xl(56.dp, 31.dp, 8.dp, 16.dp, Radius.xl, false),
    Icon(36.dp, 0.dp, 6.dp, 16.dp, Radius.md, true),
    IconXs(24.dp, 0.dp, 4.dp, 12.dp, Radius.sm, true),
    IconSm(32.dp, 0.dp, 4.dp, 16.dp, Radius.md, true),
    IconLg(44.dp, 0.dp, 6.dp, 16.dp, Radius.lg, true),
    IconXl(56.dp, 0.dp, 8.dp, 16.dp, Radius.xl, true),
    ;

    val textStyle: TextStyle
        get() = when (this) {
            Xs, IconXs -> Typography.xs
            Lg, Xl -> Typography.base
            else -> Typography.sm
        }
}

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Default,
    block: Boolean = false,
    enabled: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {
    val colors = EsuissTheme.colors
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val foreground = buttonForeground(variant, colors)
    val group = LocalButtonGroup.current?.takeIf { variant != ButtonVariant.Ghost && variant != ButtonVariant.Plain }
    RegisterButtonGroupOutline(group, variant == ButtonVariant.Outline)
    val shape = group.itemShape(if (block) RoundedCornerShape(size.blockRadius) else CircleShape)
    val sizing = when {
        block -> Modifier.fillMaxWidth().height(size.height)
        size.isIcon -> Modifier.size(size.height)
        else -> Modifier.height(size.height)
    }

    Row(
        modifier = modifier
            .then(sizing)
            .alpha(
                when {
                    !enabled -> 0.5f
                    pressed -> 0.8f
                    else -> 1f
                },
            )
            .clip(shape)
            .background(buttonBackground(variant, colors), shape)
            .then(
                when {
                    variant != ButtonVariant.Outline -> Modifier
                    group != null -> Modifier.buttonGroupEdge(group, colors.border)
                    else -> Modifier.border(1.dp, colors.border, shape)
                },
            )
            .clickable(
                interactionSource = interaction,
                indication = null,
                enabled = enabled,
                role = Role.Button,
                onClick = onClick,
            )
            .padding(horizontal = size.horizontalPadding),
        horizontalArrangement = Arrangement.spacedBy(size.spacing, Alignment.CenterHorizontally),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        CompositionLocalProvider(
            LocalButtonGroup provides null,
            LocalContentStyle provides size.textStyle.copy(
                color = foreground,
                textDecoration = if (variant == ButtonVariant.Link && pressed && enabled) {
                    TextDecoration.Underline
                } else {
                    null
                },
            ),
            LocalContentColor provides foreground,
            LocalIconSize provides size.iconSize,
        ) {
            content()
        }
    }
}

@Composable
fun Button(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Default,
    block: Boolean = false,
    enabled: Boolean = true,
) {
    Button(onClick, modifier, variant, size, block, enabled) {
        BasicText(text, style = LocalContentStyle.current, maxLines = 1)
    }
}

private fun buttonForeground(variant: ButtonVariant, colors: Colors): Color =
    when (variant) {
        ButtonVariant.Default -> colors.primaryForeground
        ButtonVariant.Outline, ButtonVariant.Secondary, ButtonVariant.Ghost ->
            colors.foreground
        ButtonVariant.Tinted, ButtonVariant.Plain ->
            colors.primary
        ButtonVariant.Destructive ->
            colors.destructive
        ButtonVariant.Link -> colors.primary
    }

private fun buttonBackground(variant: ButtonVariant, colors: Colors): Color =
    when (variant) {
        ButtonVariant.Default -> colors.primary
        ButtonVariant.Outline -> if (colors.isDark) Color.Transparent else colors.background
        ButtonVariant.Secondary -> colors.secondary
        ButtonVariant.Tinted -> colors.primary.copy(alpha = if (colors.isDark) 0.2f else 0.1f)
        ButtonVariant.Ghost, ButtonVariant.Plain, ButtonVariant.Link -> Color.Transparent
        ButtonVariant.Destructive -> colors.destructive.copy(alpha = if (colors.isDark) 0.2f else 0.1f)
    }
```

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

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

```tsx
<View className="items-start">
  <Button>
    <Text>Button</Text>
  </Button>
</View>
```

Put the label in a `Text` and icons in an `Icon`. The button passes its text color and icon size down through context, so children pick them up without extra classes.

Use an `icon` size, such as `icon` or `icon-sm`, for buttons that contain only an icon, and give them an `accessibilityLabel`.

`buttonVariants` and `buttonTextVariants` are also exported if you need the classes on another element.

## Examples

### Variants

All eight variants, plus an icon-only button.

![Preview](https://ui.suiss.app/previews/components-button--variants.light.webp)

```tsx
import { PlusIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonVariantsExample() {
  return (
    <View className="gap-3">
      <Button>
        <Text>Default</Text>
      </Button>
      <Button variant="secondary">
        <Text>Secondary</Text>
      </Button>
      <Button variant="tinted">
        <Text>Tinted</Text>
      </Button>
      <Button variant="outline">
        <Text>Outline</Text>
      </Button>
      <Button variant="ghost">
        <Text>Ghost</Text>
      </Button>
      <Button variant="plain">
        <Text>Plain</Text>
      </Button>
      <Button variant="destructive">
        <Text>Destructive</Text>
      </Button>
      <Button variant="link">
        <Text>Link</Text>
      </Button>
      <Button size="icon" variant="outline">
        <Icon as={PlusIcon} />
      </Button>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonVariantsExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      WideButton(title: "Default", variant: .default)
      WideButton(title: "Secondary", variant: .secondary)
      WideButton(title: "Tinted", variant: .tinted)
      WideButton(title: "Outline", variant: .outline)
      WideButton(title: "Ghost", variant: .ghost)
      WideButton(title: "Plain", variant: .plain)
      WideButton(title: "Destructive", variant: .destructive)
      WideButton(title: "Link", variant: .link)
      IconButton(icon: Ph.plus.regular, size: .icon, variant: .outline)
    }
  }
}

struct WideButton: View {
  let title: LocalizedStringKey
  let variant: VariantButtonStyle.Variant

  var body: some View {
    Button {} label: {
      Text(title).frame(maxWidth: .infinity)
    }
    .buttonStyle(.variant(variant))
  }
}

struct IconButton<Icon: View>: View {
  let icon: Icon
  let size: VariantButtonStyle.Size
  let variant: VariantButtonStyle.Variant

  var body: some View {
    Button {} label: {
      icon.frame(width: size.iconSize, height: size.iconSize)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text("Add"))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Plus
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon

@Composable
fun ButtonVariants() {
    val fill = Modifier.fillMaxWidth()
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Button("Default", onClick = {}, modifier = fill)
        Button("Secondary", onClick = {}, modifier = fill, variant = ButtonVariant.Secondary)
        Button("Tinted", onClick = {}, modifier = fill, variant = ButtonVariant.Tinted)
        Button("Outline", onClick = {}, modifier = fill, variant = ButtonVariant.Outline)
        Button("Ghost", onClick = {}, modifier = fill, variant = ButtonVariant.Ghost)
        Button("Plain", onClick = {}, modifier = fill, variant = ButtonVariant.Plain)
        Button("Destructive", onClick = {}, modifier = fill, variant = ButtonVariant.Destructive)
        Button("Link", onClick = {}, modifier = fill, variant = ButtonVariant.Link)
        Button(onClick = {}, variant = ButtonVariant.Outline, size = ButtonSize.Icon) {
            Icon(PhosphorIcons.Regular.Plus, contentDescription = "Add")
        }
    }
}
```

### Sizes

Compares the text and icon-only sizes.

![Preview](https://ui.suiss.app/previews/components-button--sizes.light.webp)

```tsx
import { PlusIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonSizesExample() {
  return (
    <View className="gap-3">
      <View className="flex-row flex-wrap items-center gap-3">
        <Button size="xs">
          <Text>Extra small</Text>
        </Button>
        <Button size="sm">
          <Text>Small</Text>
        </Button>
        <Button>
          <Text>Default</Text>
        </Button>
        <Button size="lg">
          <Text>Large</Text>
        </Button>
        <Button size="xl">
          <Text>Extra large</Text>
        </Button>
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        <Button size="icon-xs" variant="secondary">
          <Icon as={PlusIcon} />
        </Button>
        <Button size="icon-sm" variant="secondary">
          <Icon as={PlusIcon} />
        </Button>
        <Button size="icon" variant="secondary">
          <Icon as={PlusIcon} />
        </Button>
        <Button size="icon-lg" variant="secondary">
          <Icon as={PlusIcon} />
        </Button>
        <Button size="icon-xl" variant="secondary">
          <Icon as={PlusIcon} />
        </Button>
      </View>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonSizesExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      FlowRow {
        Button("Extra small") {}.buttonStyle(.variant(size: .xs))
        Button("Small") {}.buttonStyle(.variant(size: .sm))
        Button("Default") {}.buttonStyle(.variant())
        Button("Large") {}.buttonStyle(.variant(size: .lg))
        Button("Extra large") {}.buttonStyle(.variant(size: .xl))
      }
      FlowRow {
        IconButton(icon: Ph.plus.regular, size: .iconXs, variant: .secondary)
        IconButton(icon: Ph.plus.regular, size: .iconSm, variant: .secondary)
        IconButton(icon: Ph.plus.regular, size: .icon, variant: .secondary)
        IconButton(icon: Ph.plus.regular, size: .iconLg, variant: .secondary)
        IconButton(icon: Ph.plus.regular, size: .iconXl, variant: .secondary)
      }
    }
  }
}

struct IconButton<Icon: View>: View {
  let icon: Icon
  let size: VariantButtonStyle.Size
  let variant: VariantButtonStyle.Variant

  var body: some View {
    Button {} label: {
      icon.frame(width: size.iconSize, height: size.iconSize)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text("Add"))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Plus
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon

@Composable
fun ButtonSizes() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        FlowRow(
            horizontalArrangement = Arrangement.spacedBy(12.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp),
            itemVerticalAlignment = Alignment.CenterVertically,
        ) {
            Button("Extra small", onClick = {}, size = ButtonSize.Xs)
            Button("Small", onClick = {}, size = ButtonSize.Sm)
            Button("Default", onClick = {})
            Button("Large", onClick = {}, size = ButtonSize.Lg)
            Button("Extra large", onClick = {}, size = ButtonSize.Xl)
        }
        FlowRow(
            horizontalArrangement = Arrangement.spacedBy(12.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp),
            itemVerticalAlignment = Alignment.CenterVertically,
        ) {
            listOf(ButtonSize.IconXs, ButtonSize.IconSm, ButtonSize.Icon, ButtonSize.IconLg, ButtonSize.IconXl).forEach { size ->
                Button(onClick = {}, variant = ButtonVariant.Secondary, size = size) {
                    Icon(PhosphorIcons.Regular.Plus, contentDescription = "Add")
                }
            }
        }
    }
}
```

### With icon

Leading and trailing icons, and an icon-only button.

![Preview](https://ui.suiss.app/previews/components-button--with-icon.light.webp)

```tsx
import { ArrowRightIcon, DownloadSimpleIcon, TrashIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonWithIconExample() {
  return (
    <View className="items-start gap-3">
      <Button>
        <Icon as={DownloadSimpleIcon} />
        <Text>Download</Text>
      </Button>
      <Button variant="outline">
        <Text>Continue</Text>
        <Icon as={ArrowRightIcon} />
      </Button>
      <Button variant="destructive">
        <Icon as={TrashIcon} />
        <Text>Delete</Text>
      </Button>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonWithIconExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      Button {} label: {
        Label {
          Text("Download")
        } icon: {
          Ph.downloadSimple.regular
        }
      }
      .buttonStyle(.variant())
      Button {} label: {
        HStack(spacing: 6) {
          Text("Continue")
          Ph.arrowRight.regular
            .frame(width: VariantButtonStyle.Size.default.iconSize, height: VariantButtonStyle.Size.default.iconSize)
        }
      }
      .buttonStyle(.variant(.outline))
      Button {} label: {
        Label {
          Text("Delete")
        } icon: {
          Ph.trash.regular
        }
      }
      .buttonStyle(.variant(.destructive))
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowRight
import com.adamglin.phosphoricons.regular.DownloadSimple
import com.adamglin.phosphoricons.regular.Trash
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text

@Composable
fun ButtonWithIcon() {
    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Button(onClick = {}) {
            Icon(PhosphorIcons.Regular.DownloadSimple, contentDescription = null)
            Text("Download")
        }
        Button(onClick = {}, variant = ButtonVariant.Outline) {
            Text("Continue")
            Icon(PhosphorIcons.Regular.ArrowRight, contentDescription = null)
        }
        Button(onClick = {}, variant = ButtonVariant.Destructive) {
            Icon(PhosphorIcons.Regular.Trash, contentDescription = null)
            Text("Delete")
        }
    }
}
```

### Disabled

Disabled buttons in every variant.

![Preview](https://ui.suiss.app/previews/components-button--disabled.light.webp)

```tsx
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"

export function ButtonDisabledExample() {
  return (
    <View className="items-start gap-3">
      <Button disabled>
        <Text>Default</Text>
      </Button>
      <Button disabled variant="secondary">
        <Text>Secondary</Text>
      </Button>
      <Button disabled variant="tinted">
        <Text>Tinted</Text>
      </Button>
      <Button disabled variant="outline">
        <Text>Outline</Text>
      </Button>
      <Button disabled variant="destructive">
        <Text>Destructive</Text>
      </Button>
    </View>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct ButtonDisabledExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      Button("Default") {}.buttonStyle(.variant())
      Button("Secondary") {}.buttonStyle(.variant(.secondary))
      Button("Tinted") {}.buttonStyle(.variant(.tinted))
      Button("Outline") {}.buttonStyle(.variant(.outline))
      Button("Destructive") {}.buttonStyle(.variant(.destructive))
    }
    .disabled(true)
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant

@Composable
fun ButtonDisabled() {
    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Button("Default", onClick = {}, enabled = false)
        Button("Secondary", onClick = {}, variant = ButtonVariant.Secondary, enabled = false)
        Button("Tinted", onClick = {}, variant = ButtonVariant.Tinted, enabled = false)
        Button("Outline", onClick = {}, variant = ButtonVariant.Outline, enabled = false)
        Button("Destructive", onClick = {}, variant = ButtonVariant.Destructive, enabled = false)
    }
}
```

### Block

Full-width buttons at each size, whose corner radius scales with size.

![Preview](https://ui.suiss.app/previews/components-button--block.light.webp)

```tsx
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"

export function ButtonBlockExample() {
  return (
    <View className="gap-3">
      <Button block size="lg">
        <Text>Continue</Text>
      </Button>
      <Button block size="lg" variant="secondary">
        <Text>Not now</Text>
      </Button>
      <Button block variant="tinted">
        <Text>Learn more</Text>
      </Button>
    </View>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct ButtonBlockExample: View {
  var body: some View {
    VStack(spacing: 12) {
      Button("Continue") {}.buttonStyle(.variant(size: .lg, block: true))
      Button("Not now") {}.buttonStyle(.variant(.secondary, size: .lg, block: true))
      Button("Learn more") {}.buttonStyle(.variant(.tinted, block: true))
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant

@Composable
fun ButtonBlock() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Button("Continue", onClick = {}, size = ButtonSize.Lg, block = true)
        Button("Not now", onClick = {}, size = ButtonSize.Lg, variant = ButtonVariant.Secondary, block = true)
        Button("Learn more", onClick = {}, variant = ButtonVariant.Tinted, block = true)
    }
}
```

## API Reference

### Button

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline" \| "secondary" \| "tinted" \| "ghost" \| "plain" \| "destructive" \| "link"` | `"default"` | Visual style. |
| `size` | `"xs" \| "sm" \| "default" \| "lg" \| "xl" \| "icon-xs" \| "icon-sm" \| "icon" \| "icon-lg" \| "icon-xl"` | `"default"` | Height and padding; `icon` sizes are square. |
| `block` | `boolean` | - | Stretches the button to the full width of its container. |

Also accepts the props of `ButtonProps`.

## Accessibility

- Renders a `Pressable` with `role="button"`, so VoiceOver and TalkBack announce it as a button.
- `disabled` is announced and blocks presses.
- Icon-only buttons need an `accessibilityLabel`.

## Notes

- Colors come from semantic tokens such as `bg-primary`, `bg-secondary`, `bg-primary/10 dark:bg-primary/20` and `bg-destructive/10 dark:bg-destructive/20`, with `opacity-80` while pressed and `opacity-50` when disabled.
- Light and dark colors follow the system appearance.

