# Button

> Puffy clay button that squishes when you press it.

Docs: https://ui.parlemen.dev/docs/button

## Installation

```bash
npx shadcn@latest add @parlemen/button
```

## Usage

```tsx
import { FloppyDiskIcon, HeartIcon, PaperPlaneTiltIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react/dist/ssr";
import { Button } from "@/components/ui/button";

const colors = ["primary", "secondary", "tertiary", "accent", "danger", "neutral"] as const;
const variants = ["solid", "soft", "outline", "ghost"] as const;

export default function ButtonDemo() {
  return (
    <div className="grid gap-4">
      {variants.map((variant) => (
        <div key={variant} className="flex flex-wrap items-center gap-3">
          {colors.map((color) => (
            <Button key={color} variant={variant} color={color} size="sm" className="capitalize">
              {color}
            </Button>
          ))}
        </div>
      ))}
      <div className="mt-2 flex flex-wrap items-center gap-3">
        <Button><PaperPlaneTiltIcon weight="bold" />Send</Button>
        <Button variant="soft" color="secondary"><FloppyDiskIcon weight="bold" />Save draft</Button>
        <Button variant="soft" color="danger"><TrashIcon weight="bold" />Delete</Button>
        <Button size="icon" color="neutral" aria-label="Add"><PlusIcon weight="bold" /></Button>
        <Button size="icon" color="primary" aria-label="Like"><HeartIcon weight="fill" /></Button>
      </div>
    </div>
  );
}
```

## Source (components/ui/button.tsx)

```tsx
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { motion, type HTMLMotionProps } from "motion/react";
import { cva, type VariantProps } from "class-variance-authority";
import { springs } from "@/lib/motion";
import { cn } from "@/lib/utils";

const colors = ["primary", "secondary", "tertiary", "accent", "danger", "neutral"] as const;

// Each color role maps to its token family. `neutral` uses surface tokens.
const solid: Record<(typeof colors)[number], string> = {
  primary: "bg-primary text-on-primary",
  secondary: "bg-secondary text-on-secondary",
  tertiary: "bg-tertiary text-on-tertiary",
  accent: "bg-accent text-on-accent",
  danger: "bg-danger text-on-danger",
  neutral: "bg-surface text-on-surface",
};
const soft: typeof solid = {
  primary: "bg-primary-container text-on-primary-container",
  secondary: "bg-secondary-container text-on-secondary-container",
  tertiary: "bg-tertiary-container text-on-tertiary-container",
  accent: "bg-accent-container text-on-accent-container",
  danger: "bg-danger-container text-on-danger-container",
  neutral: "bg-surface-sunken text-on-surface",
};
const text: typeof solid = {
  primary: "text-on-primary-container",
  secondary: "text-on-secondary-container",
  tertiary: "text-on-tertiary-container",
  accent: "text-on-accent-container",
  danger: "text-on-danger-container",
  neutral: "text-on-surface",
};

const buttonVariants = cva(
  "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-full font-bold transition-[box-shadow,background-color] duration-150 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-[1.15em] [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        solid: "shadow-clay active:shadow-clay-pressed",
        soft: "shadow-clay-sm active:shadow-clay-pressed",
        outline: "bg-surface shadow-clay-sm ring-2 ring-inset ring-current/25 active:shadow-clay-pressed",
        ghost: "bg-transparent hover:bg-surface-sunken",
      },
      color: Object.fromEntries(colors.map((c) => [c, ""])) as Record<(typeof colors)[number], string>,
      size: {
        sm: "h-9 px-4 text-sm",
        default: "h-11 px-6 text-base",
        lg: "h-14 px-8 text-lg",
        icon: "size-11",
      },
    },
    compoundVariants: colors.flatMap((color) => [
      { variant: "solid" as const, color, className: solid[color] },
      { variant: "soft" as const, color, className: soft[color] },
      { variant: "outline" as const, color, className: text[color] },
      { variant: "ghost" as const, color, className: text[color] },
    ]),
    defaultVariants: { variant: "solid", color: "primary", size: "default" },
  }
);

export interface ButtonProps
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color">,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

/** Clay squish: lifts on hover, flattens and spreads when pressed, springs back. */
const press = {
  whileHover: { y: -2 },
  whileTap: { y: 1, scaleY: 0.9, scaleX: 1.05 },
  transition: springs.squish,
};

export function Button({ className, variant, color, size, asChild, ...props }: ButtonProps) {
  const classes = cn(buttonVariants({ variant, color, size }), className);
  if (asChild) {
    // Slot cannot take motion props, so links get the same feel from CSS.
    return <Slot className={cn(classes, "transition-[box-shadow,background-color,translate,scale] hover:-translate-y-0.5 active:translate-y-0.5 active:scale-y-95")} {...props} />;
  }
  const isGhost = variant === "ghost";
  return (
    <motion.button
      className={classes}
      {...(isGhost ? { whileTap: { scale: 0.94 }, transition: springs.squish } : press)}
      {...(props as HTMLMotionProps<"button">)}
    />
  );
}
export { buttonVariants };
```
