Components / Primitives

Button

The action primitive: presses in and settles back — six variants, eight sizes, composes through render.

variant
size
icon
pnpm exec shadcn add @intelligo/button

Inside an app made by intelligo create, or any shadcn project that names the registry.

Source

What shadcn add writes to components/ui/button.tsx. It is yours from then on.

/*
 * The button: a control that presses in and settles back, over Base UI's
 * Button so `render` keeps working for links and triggers. The press is a
 * spring (`Press` from ai-motion, a client component) with an optional
 * ripple; a button given its own `render` — a link, a trigger's element —
 * keeps the CSS press. This module stays free of "use client" so a server
 * component can import `buttonVariants`.
 */

import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";

import { Press } from "@showcase/components/ui/ai-motion";
import { cn } from "@showcase/lib/utils";

const buttonVariants = cva(
  "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,scale,opacity] duration-fast ease-standard outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/40 active:not-aria-[haspopup]:scale-97 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 in-data-[slot=button-group]:active:scale-100 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/88",
        outline:
          "border-border bg-transparent hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent aria-expanded:text-accent-foreground",
        secondary:
          "border-border bg-card text-card-foreground hover:border-input hover:bg-accent aria-expanded:bg-accent",
        ghost:
          "text-muted-foreground hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent aria-expanded:text-accent-foreground",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/16 focus-visible:border-destructive/40 focus-visible:ring-destructive/20",
        link: "rounded-md text-primary underline-offset-4 hover:underline active:not-aria-[haspopup]:scale-100",
      },
      size: {
        default:
          "h-8 gap-1.5 px-3.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
        xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-7 gap-1 px-3 text-xs has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-3.5",
        lg: "h-10 gap-2 px-5 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
        icon: "size-8",
        "icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
        "icon-sm": "size-7",
        "icon-lg": "size-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

function Button({
  className,
  variant = "default",
  size = "default",
  ripple = false,
  render,
  ...props
}: ButtonPrimitive.Props &
  VariantProps<typeof buttonVariants> & {
    /** Spread a ripple from the press point (spring-pressed buttons only). */
    ripple?: boolean;
  }) {
  const springs = render === undefined;

  return (
    <ButtonPrimitive
      data-slot="button"
      render={
        springs ? (
          <Press ripple={ripple} pressScale={variant === "link" ? 1 : 0.97} />
        ) : (
          render
        )
      }
      className={cn(
        buttonVariants({ variant, size }),
        // The spring owns the press; the CSS one stays for custom elements.
        springs && "active:not-aria-[haspopup]:scale-100",
        className
      )}
      {...props}
    />
  );
}

export { Button, buttonVariants };

Ready to try it? Three commands and the app runs against a stub model.