Docs
Button

Button

A button allows a user to perform an action, with mouse, touch, and keyboard interactions.

Loading...

Installation

Copy and paste the following code into your project.

"use client";
 
import React, { createContext, useContext, useMemo } from "react";
import { Slot } from "@radix-ui/react-slot";
import type { VariantProps } from "cva";
import { cva } from "cva";
import { Loader2 } from "lucide-react";
import { useButton } from "react-aria";
import type { Button as RaButton } from "react-aria-components";
import {
  ButtonContext as RaButtonContext,
  useContextProps,
} from "react-aria-components";
 
import { autoRef, cn } from "@/lib/utils";
 
/* -------------------------------------------------------------------------- */
/*                                   Context                                  */
/* -------------------------------------------------------------------------- */
 
interface ButtonContextType {
  isLoading?: boolean;
  iconOnly?: boolean;
  size?: VariantProps<typeof buttonVariants>["size"];
}
 
const ButtonContext = createContext<ButtonContextType | null>(null);
 
function useButtonContext() {
  const context = useContext(ButtonContext);
  if (!context) {
    throw new Error("useButtonContext must be used within a <Button/>");
  }
  return context;
}
 
/* -------------------------------------------------------------------------- */
/*                                 Components                                 */
/* -------------------------------------------------------------------------- */
 
/* ---------------------------------- Root ---------------------------------- */
 
export const buttonVariants = cva({
  base: cn(
    "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors",
    "focus-visible:outline-none",
    "disabled:pointer-events-none disabled:opacity-50",
  ),
  variants: {
    variant: {
      default: "",
      secondary: "",
      outline: "border bg-background",
      ghost: "",
    },
    intent: {
      default: "",
      destructive: "",
    },
    size: {
      xs: "h-8 px-2 text-xs",
      sm: "h-9 px-3",
      default: "h-10 px-3",
      lg: "h-11 px-5",
    },
    icon: {
      true: "",
      false: "",
    },
    width: {
      default: "inline-flex",
      full: "flex w-full",
    },
  },
  compoundVariants: [
    // default variant
    {
      variant: "default",
      intent: "default",
      className: "bg-primary text-primary-foreground hover:bg-primary/90",
    },
    {
      variant: "default",
      intent: "destructive",
      className:
        "bg-destructive text-destructive-foreground hover:bg-destructive/90",
    },
    // secondary variant
    {
      variant: "secondary",
      intent: "default",
      className: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
    },
    // outline variant
    {
      variant: "outline",
      intent: "default",
      className: "border-input hover:bg-accent",
    },
    {
      variant: "outline",
      intent: "destructive",
      className: "border-destructive text-destructive hover:bg-destructive/5",
    },
    // ghost variant
    {
      variant: "ghost",
      intent: "default",
      className: "hover:bg-accent",
    },
    {
      variant: "ghost",
      intent: "destructive",
      className: "text-destructive hover:bg-destructive/10",
    },
    // icon only
    {
      size: "sm",
      icon: true,
      className: "h-9 w-9 p-2",
    },
    {
      size: "default",
      icon: true,
      className: "h-10 w-10 px-0",
    },
    {
      size: "lg",
      icon: true,
      className: "h-11 w-11 px-0",
    },
  ],
  defaultVariants: {
    variant: "default",
    intent: "default",
    size: "default",
    width: "default",
    icon: false,
  },
});
 
export interface ButtonProps
  extends Omit<React.ComponentPropsWithRef<typeof RaButton>, "children">,
    VariantProps<typeof buttonVariants> {
  children?: React.ReactNode;
  isLoading?: boolean;
  /**
   * @deprecated use `onPress` instead. onClick is only defined for compatibility with third party libraries.
   */
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
  onPointerDown?: React.PointerEventHandler<HTMLButtonElement>;
}
 
export const Button = autoRef(({ ref, ...props }: ButtonProps) => {
  [props, ref] = useContextProps(props, ref!, RaButtonContext);
 
  const {
    children,
    className,
    width,
    variant,
    size,
    icon,
    intent,
    isLoading,
    isDisabled,
    type,
  } = props;
  const buttonContextValues = useMemo(
    () => ({
      isLoading,
      iconOnly: icon,
      size,
    }),
    [isLoading, icon, size],
  );
 
  const hasIcon = React.Children.toArray(children).some(
    (child) => React.isValidElement(child) && child.type === ButtonIcon,
  );
 
  // have to use react-aria's useButton hook because of the way it handles press events
  // if any third party library injects "onClick" or "onPointerDown" props (e.g. radix)
  // it would not be propagated to the button
  const { onPointerDown, onClick, ...sanitizedProps } = props;
  const { buttonProps } = useButton(
    sanitizedProps,
    ref as React.RefObject<Element>,
  );
 
  const { onPointerDown: ariaOnPointerDown, onClick: ariaOnClick } =
    buttonProps;
 
  return (
    <ButtonContext.Provider value={buttonContextValues}>
      <button
        {...buttonProps}
        className={cn(
          buttonVariants({
            variant,
            size,
            width,
            icon,
            intent,
          }),
          className,
        )}
        onPointerDown={(e) => {
          if (onPointerDown !== undefined) {
            onPointerDown(e);
          }
 
          ariaOnPointerDown?.(e);
        }}
        onClick={(e) => {
          if (onClick !== undefined) {
            onClick(e);
          }
 
          ariaOnClick?.(e);
        }}
        disabled={isLoading || isDisabled}
        type={props.form !== undefined ? "submit" : type}
        form={props.form}
        ref={ref}
      >
        <>
          {!hasIcon && <ButtonLoader />}
          {children}
        </>
      </button>
    </ButtonContext.Provider>
  );
});
 
/* ---------------------------------- Icon ---------------------------------- */
 
export const ButtonIcon = ({
  icon,
  className,
  only,
}: {
  icon?: React.ReactNode;
  className?: string;
  only?: boolean;
}) => {
  const { isLoading, iconOnly } = useButtonContext();
  const isIconOnly = iconOnly || only;
 
  if (isLoading) {
    return (
      <Loader2
        aria-hidden="true"
        className={cn("h-4 w-4 animate-spin", !isIconOnly && "mr-2", className)}
      />
    );
  }
 
  return (
    <Slot
      className={cn("h-4 w-4", !isIconOnly && "mr-2", className)}
      aria-hidden="true"
    >
      {icon}
    </Slot>
  );
};
 
/* --------------------------------- Loader --------------------------------- */
 
export const ButtonLoader = ({ show }: { show?: boolean }) => {
  const { isLoading } = useButtonContext();
 
  if (isLoading ?? show) {
    return <Loader2 aria-hidden="true" className="mr-2 h-4 w-4 animate-spin" />;
  }
 
  return null;
};
 
/* ------------------------------- CloseDialog ------------------------------ */
 
export type CloseDialogButtonProps = Omit<
  React.ComponentPropsWithRef<typeof Button>,
  "onPress"
> & {
  close: () => void;
};
 
export const CloseDialogButton = ({
  close,
  children,
  variant,
  ...props
}: CloseDialogButtonProps) => {
  return (
    <Button {...props} onPress={close} variant={variant ?? "outline"}>
      {children ?? "Abbrechen"}
    </Button>
  );
};

Update the import paths to match your project setup.

You can use the buttonVariants helper to create a link that looks like a button.

import { buttonVariants } from "@/components/ui/button";
<Link className={buttonVariants({ variant: "outline" })}>Click here</Link>