Button

The Button component provides a consistent way to trigger actions across the application.

Dependencies

Source Code

"use client";
 
import { VariantProps } from "cva";
import { Slot, Slottable } from "@/components/slot";
 
import { cn, cva } from "@/lib/utils";
 
import { Spinner } from "@/components/spinner";
 
const buttonStyle = cva({
  base: "shrink-0 relative whitespace-nowrap inline-flex items-center justify-center gap-1.5 font-medium shadow-xs transition focus-visible:outline-none focus-visible:ring-4 disabled:opacity-40 enabled:cursor-pointer h-(--button-height) ring-ring active:scale-98 text-(--button-text-color) [--button-text-color:var(--color-foreground)]",
  variants: {
    variant: {
      primary: "bg-foreground [--button-text-color:var(--color-background)]",
      outline: "border border-border bg-background",
      ghost:
        "border-none bg-transparent ring-0 shadow-none hover:bg-foreground/5",
      destructive:
        "bg-red-600 [--button-text-color:var(--color-white)] ring-red-600/50 hover:bg-red-700",
    },
    size: {
      xs: "rounded-lg px-2 text-sm [--button-height:--spacing(6)]",
      sm: "rounded-lg px-3 text-sm [--button-height:--spacing(8)]",
      md: "rounded-xl px-4 text-base [--button-height:--spacing(10)]",
      lg: "rounded-2xl px-5 text-base [--button-height:--spacing(12)]",
    },
    square: {
      true: "w-(--button-height) px-0",
      false: "",
    },
  },
  defaultVariants: {
    variant: "primary",
    size: "md",
  },
});
 
export interface ButtonProps
  extends React.ComponentPropsWithRef<"button">,
    VariantProps<typeof buttonStyle> {
  asChild?: boolean;
  isLoading?: boolean;
}
 
const Button = ({
  children,
  className,
  variant,
  asChild = false,
  isLoading,
  size = "md",
  square,
  type = "button",
  ref,
  ...props
}: ButtonProps) => {
  const Comp = asChild ? Slot : "button";
 
  return (
    <Comp
      className={cn(
        buttonStyle({
          className,
          variant,
          size,
          square,
        }),
        isLoading && "text-transparent transition-none"
      )}
      ref={ref}
      type={type}
      {...props}
    >
      <Slottable asChild={asChild} child={children}>
        {(child) => (
          <>
            {child}
            {isLoading && (
              <span
                data-button-spinner
                className={cn(
                  "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
                  "text-(--button-text-color)"
                )}
              >
                <Spinner size={size} />
              </span>
            )}
          </>
        )}
      </Slottable>
    </Comp>
  );
};
 
export { Button, buttonStyle };

API Reference

PropDefaultTypeDescription

variant

"primary"

"primary"

"outline"

"ghost"

"destructive"

-

size

"md"

"xs"

"sm"

"md"

"lg"

-

square

false

boolean

Makes the button a square. Helpful when you have a button with only an icon inside.

isLoading

false

boolean

Replaces the button content with a spinner

asChild

-

boolean

-

Examples

Sizes

Variants

Loading

Icons

disabled

Best Practices

  1. Variants:

    • Use primary for main actions
    • Use destructive for dangerous actions
    • Use ghost for subtle actions
  2. Accessibility:

    • Ensure clear button text
    • Consider loading states