Button

Categorystyled
ArchitectureReact Server Component
Dependencies

Usage

Controls
Color selector
import { useState } from "react";

//----- Sub-Components -----//
import { Button } from "@/components/react/styled/button";
import { DemoWrapper } from "@/demos/react/demoWrapper";
import { CameraIcon, QrCodeIcon } from "@heroicons/react/24/outline";

//----- Types -----//
import type { ButtonVariants, Colors } from "@/types";
import type { ControlValuesInterface } from "./controls";

//----- Internal Constants -----//
const buttonVariants: ButtonVariants[] = [
  "filled",
  "ghost",
  "outline",
  "subtle",
  "underline",
];
const colorsVariants: Colors[] = [
  "theme",
  "danger",
  "dark",
  "light",
  "link",
  "primary",
  "successful",
  "warning",
];

//----- Exported Component -----//
export const ButtonDemo = ({ code }: { code: string }) => {
  const [controlValues, setControlValues] = useState<ControlValuesInterface>({
    variant: "subtle",
    color: "theme",
    compact: false,
    square: false,
    loading: false,
    iconRight: false,
    iconLeft: false,
    innerText: "Click me",
  });

  return (
    <DemoWrapper
      code={code}
      controls={{
        variant: {
          type: "enum",
          values: buttonVariants,
          defaultValue: controlValues["variant"] as string,
          id: "variant",
          label: "Variant",
          gridSpan: 2,
        },
        color: {
          type: "color",
          values: colorsVariants,
          defaultValue: controlValues["color"] as Colors,
          id: "color",
          label: "Color selector",
        },
        compact: {
          type: "boolean",
          defaultValue: controlValues["compact"] as boolean,
          id: "compact",
          label: "Compact",
        },
        square: {
          type: "boolean",
          defaultValue: controlValues["square"] as boolean,
          id: "square",
          label: "Square",
        },
        iconRight: {
          type: "boolean",
          defaultValue: controlValues["iconRight"] as boolean,
          id: "iconRight",
          label: "Icon Right",
          disabled: controlValues["square"] === true,
        },
        iconLeft: {
          type: "boolean",
          defaultValue: controlValues["iconLeft"] as boolean,
          id: "iconLeft",
          label: "Icon Left",
          disabled: controlValues["square"] === true,
        },
        loading: {
          type: "boolean",
          defaultValue: controlValues["loading"] as boolean,
          id: "loading",
          label: "Loading",
        },
        innerText: {
          type: "string",
          defaultValue: controlValues["innerText"] as string,
          id: "innerText",
          label: "Inner Text",
        },
      }}
      setState={setControlValues}
    >
      <Button
        variant={controlValues["variant"] as ButtonVariants}
        color={controlValues["color"] as Colors}
        compact={controlValues["compact"] as boolean}
        square={controlValues["square"] as boolean}
        loading={controlValues["loading"] as boolean}
        iconLeft={
          controlValues["square"] === false &&
          controlValues["iconLeft"] && <CameraIcon className="w-4 h-4" />
        }
        iconRight={
          controlValues["square"] === false &&
          controlValues["iconRight"] && <QrCodeIcon className="w-4 h-4" />
        }
      >
        {controlValues["square"] === false ? (
          controlValues["innerText"]
        ) : (
          <CameraIcon className="w-4 h-4" />
        )}
      </Button>
    </DemoWrapper>
  );
};

Install Instructions

Verifying dependencies

Make sure the following dependencies are satisfied:

  • loading

Copy and paste the code into that file

import {
  type ReactNode,
  type ComponentProps,
  type ElementType,
  createElement,
  Fragment,
} from "react";

// Types
import type { VariantProps } from "class-variance-authority";

// CVA
import { cva } from "class-variance-authority";

// TW Merge
import { twMerge } from "tailwind-merge";

// Sub Components
import { Loading } from "@/components/react/styled/loading";

const borderColor = "border-[var(--border-color)]";

export const buttonClassName = cva(
  [
    "text-center",
    "capitalize",
    "py-2",
    "px-5",
    "text-sm",
    "inline-flex",
    "justify-center",
    "items-center",
    "height",
    "leading-none",
    "transition-colors",
    "duration-200",
    "rounded-3xl",
    "border",
    "border-transparent",
    "gap-2",
    "hover:cursor-pointer",
    "disabled:opacity-75",
    "disabled:hover:cursor-not-allowed",
    "disabled:data-[loading=true]:hover:cursor-wait",
    "relative",
  ],
  {
    variants: {
      variant: {
        filled: ["no-underline"],
        outline: ["no-underline", borderColor, "text-50", "dark:text-950"],
        ghost: ["no-underline"],
        subtle: ["no-underline", "text-50", "dark:text-950"],
        underline: ["underline", "underline-offset-2"],
      },
      compact: {
        true: ["py-1", "px-3"],
        false: [],
      },
      square: {
        true: ["p-2"],
        false: [],
      },
      compiledVariant: {
        //* Theme
        "theme-filled": [
          "text-neutral-950",
          "fill-neutral-950",
          "bg-neutral-300",
          "hover:bg-neutral-200",
          "focus-visible:bg-neutral-200",
          "active:bg-neutral-100",
          "dark:text-neutral-50",
          "dark:fill-neutral-50",
          "dark:bg-neutral-800",
          "dark:hover:bg-neutral-700",
          "dark:focus-visible:bg-neutral-700",
          "dark:active:bg-neutral-600",
        ],
        "theme-subtle": [
          "text-neutral-950",
          "fill-neutral-950",
          "bg-neutral-300/50",
          "hover:bg-neutral-200/50",
          "focus-visible:bg-neutral-200/50",
          "active:bg-neutral-100/50",
          "dark:text-neutral-50",
          "dark:fill-neutral-50",
          "dark:bg-neutral-800/50",
          "dark:hover:bg-neutral-700/50",
          "dark:focus-visible:bg-neutral-700/50",
          "dark:active:bg-neutral-600/50",
        ],
        "theme-ghost": [
          "text-neutral-900",
          "fill-neutral-900",
          "hover:text-neutral-800",
          "hover:fill-neutral-800",
          "focus-visible:text-neutral-800",
          "focus-visible:fill-neutral-800",
          "active:text-neutral-700",
          "active:fill-neutral-700",
          "dark:text-neutral-100",
          "dark:fill-neutral-100",
          "dark:hover:text-neutral-200",
          "dark:hover:fill-neutral-200",
          "dark:focus-visible:text-neutral-200",
          "dark:focus-visible:fill-neutral-200",
          "dark:active:text-neutral-300",
          "dark:active:fill-neutral-300",
        ],

        //* Light
        "light-filled": [
          "text-neutral-950",
          "fill-neutral-950",
          "bg-neutral-300",
          "hover:bg-neutral-200",
          "focus-visible:bg-neutral-200",
          "active:bg-neutral-100",
        ],
        "light-subtle": [
          "text-neutral-950",
          "fill-neutral-950",
          "bg-neutral-300/50",
          "hover:bg-neutral-200/50",
          "focus-visible:bg-neutral-200/50",
          "active:bg-neutral-100/50",
        ],
        "light-ghost": [
          "text-neutral-900",
          "fill-neutral-900",
          "hover:text-neutral-800",
          "hover:fill-neutral-800",
          "focus-visible:text-neutral-800",
          "focus-visible:fill-neutral-800",
          "active:text-neutral-700",
          "active:fill-neutral-700",
        ],

        //* Dark
        "dark-filled": [
          "text-neutral-50",
          "fill-neutral-50",
          "bg-neutral-800",
          "hover:bg-neutral-700",
          "focus-visible:bg-neutral-700",
          "active:bg-neutral-600",
        ],
        "dark-subtle": [
          "text-neutral-50",
          "fill-neutral-50",
          "bg-neutral-800/50",
          "hover:bg-neutral-700/50",
          "focus-visible:bg-neutral-700/50",
          "active:bg-neutral-600/50",
        ],
        "dark-ghost": [
          "text-neutral-100",
          "fill-neutral-100",
          "hover:text-neutral-200",
          "hover:fill-neutral-200",
          "focus-visible:text-neutral-200",
          "focus-visible:fill-neutral-200",
          "active:text-neutral-300",
          "active:fill-neutral-300",
        ],

        //* Primary
        "primary-filled": [
          "text-primary-100",
          "fill-primary-100",
          "bg-primary-500",
          "hover:bg-primary-600",
          "focus-visible:bg-primary-600",
          "active:bg-primary-700",
          "dark:text-primary-950",
          "dark:fill-primary-950",
          "dark:bg-primary-400",
          "dark:hover:bg-primary-500",
          "dark:focus-visible:bg-primary-500",
          "dark:active:bg-primary-600",
        ],
        "primary-subtle": [
          "text-primary-950",
          "fill-primary-950",
          "bg-primary-500/50",
          "hover:bg-primary-600/50",
          "focus-visible:bg-primary-600/50",
          "active:bg-primary-700/50",
          "dark:text-primary-100",
          "dark:fill-primary-100",
          "dark:bg-primary-400/50",
          "dark:hover:bg-primary-500/50",
          "dark:focus-visible:bg-primary-500/50",
          "dark:active:bg-primary-600/50",
        ],
        "primary-ghost": [
          "text-primary-600",
          "hover:text-primary-700",
          "focus-visible:text-primary-700",
          "active:text-primary-800",
          "dark:text-primary-400",
          "dark:hover:text-primary-500",
          "dark:focus-visible:text-primary-500",
          "dark:active:text-primary-600",
        ],

        //* Danger
        "danger-filled": [
          "text-red-100",
          "fill-red-100",
          "bg-red-500",
          "hover:bg-red-600",
          "focus-visible:bg-red-600",
          "active:bg-red-700",
          "dark:text-red-950",
          "dark:fill-red-950",
          "dark:bg-red-400",
          "dark:hover:bg-red-500",
          "dark:focus-visible:bg-red-500",
          "dark:active:bg-red-600",
        ],
        "danger-subtle": [
          "text-red-950",
          "fill-red-950",
          "bg-red-500/50",
          "hover:bg-red-600/50",
          "focus-visible:bg-red-600/50",
          "active:bg-red-700/50",
          "dark:text-red-100",
          "dark:fill-red-100",
          "dark:bg-red-400/50",
          "dark:hover:bg-red-500/50",
          "dark:focus-visible:bg-red-500/50",
          "dark:active:bg-red-600/50",
        ],
        "danger-ghost": [
          "text-red-600",
          "hover:text-red-700",
          "focus-visible:text-red-700",
          "active:text-red-800",
          "dark:text-red-400",
          "dark:hover:text-red-500",
          "dark:focus-visible:text-red-500",
          "dark:active:text-red-600",
        ],

        //* Link
        "link-filled": [
          "text-blue-100",
          "fill-blue-100",
          "bg-blue-500",
          "hover:bg-blue-600",
          "focus-visible:bg-blue-600",
          "active:bg-blue-700",
          "dark:text-blue-950",
          "dark:fill-blue-950",
          "dark:bg-blue-400",
          "dark:hover:bg-blue-500",
          "dark:focus-visible:bg-blue-500",
          "dark:active:bg-blue-600",
        ],
        "link-subtle": [
          "text-blue-950",
          "fill-blue-950",
          "bg-blue-500/50",
          "hover:bg-blue-600/50",
          "focus-visible:bg-blue-600/50",
          "active:bg-blue-700/50",
          "dark:text-blue-100",
          "dark:fill-blue-100",
          "dark:bg-blue-400/50",
          "dark:hover:bg-blue-500/50",
          "dark:focus-visible:bg-blue-500/50",
          "dark:active:bg-blue-600/50",
        ],
        "link-ghost": [
          "text-blue-600",
          "hover:text-blue-700",
          "focus-visible:text-blue-700",
          "active:text-blue-800",
          "dark:text-blue-400",
          "dark:hover:text-blue-500",
          "dark:focus-visible:text-blue-500",
          "dark:active:text-blue-600",
        ],

        //* Successful
        "successful-filled": [
          "text-green-100",
          "fill-green-100",
          "bg-green-500",
          "hover:bg-green-600",
          "focus-visible:bg-green-600",
          "active:bg-green-700",
          "dark:text-green-950",
          "dark:fill-green-950",
          "dark:bg-green-400",
          "dark:hover:bg-green-500",
          "dark:focus-visible:bg-green-500",
          "dark:active:bg-green-600",
        ],
        "successful-subtle": [
          "text-green-950",
          "fill-green-950",
          "bg-green-500/50",
          "hover:bg-green-600/50",
          "focus-visible:bg-green-600/50",
          "active:bg-green-700/50",
          "dark:text-green-100",
          "dark:fill-green-100",
          "dark:bg-green-400/50",
          "dark:hover:bg-green-500/50",
          "dark:focus-visible:bg-green-500/50",
          "dark:active:bg-green-600/50",
        ],
        "successful-ghost": [
          "text-green-600",
          "hover:text-green-700",
          "focus-visible:text-green-700",
          "active:text-green-800",
          "dark:text-green-400",
          "dark:hover:text-green-500",
          "dark:focus-visible:text-green-500",
          "dark:active:text-green-600",
        ],

        //* Warning
        "warning-filled": [
          "text-yellow-100",
          "fill-yellow-100",
          "bg-yellow-500",
          "hover:bg-yellow-600",
          "focus-visible:bg-yellow-600",
          "active:bg-yellow-700",
          "dark:text-yellow-950",
          "dark:fill-yellow-950",
          "dark:bg-yellow-400",
          "dark:hover:bg-yellow-500",
          "dark:focus-visible:bg-yellow-500",
          "dark:active:bg-yellow-600",
        ],
        "warning-subtle": [
          "text-yellow-950",
          "fill-yellow-950",
          "bg-yellow-500/50",
          "hover:bg-yellow-600/50",
          "focus-visible:bg-yellow-600/50",
          "active:bg-yellow-700/50",
          "dark:text-yellow-100",
          "dark:fill-yellow-100",
          "dark:bg-yellow-400/50",
          "dark:hover:bg-yellow-500/50",
          "dark:focus-visible:bg-yellow-500/50",
          "dark:active:bg-yellow-600/50",
        ],
        "warning-ghost": [
          "text-yellow-500",
          "hover:text-yellow-600",
          "focus-visible:text-yellow-600",
          "active:text-yellow-700",
          "dark:text-yellow-400",
          "dark:hover:text-yellow-500",
          "dark:focus-visible:text-yellow-500",
          "dark:active:text-yellow-600",
        ],
      },
      color: {
        theme: [
          "[--border-color:_theme(colors.neutral.300)]",
          "hover:[--border-color:_theme(colors.neutral.200)]",
          "focus-visible:[--border-color:_theme(colors.neutral.200)]",
          "active:[--border-color:_theme(colors.neutral.100)]",
          "dark:[--border-color:_theme(colors.neutral.800)]",
          "dark:hover:[--border-color:_theme(colors.neutral.700)]",
          "dark:focus-visible:[--border-color:_theme(colors.neutral.700)]",
          "dark:active:[--border-color:_theme(colors.neutral.600)]",
        ],
        light: [
          "[--border-color:_theme(colors.neutral.300)]",
          "hover:[--border-color:_theme(colors.neutral.200)]",
          "focus-visible:[--border-color:_theme(colors.neutral.200)]",
          "active:[--border-color:_theme(colors.neutral.100)]",
        ],
        dark: [
          "[--border-color:_theme(colors.neutral.800)]",
          "hover:[--border-color:_theme(colors.neutral.700)]",
          "focus-visible:[--border-color:_theme(colors.neutral.700)]",
          "active:[--border-color:_theme(colors.neutral.600)]",
        ],
        primary: [
          "[--border-color:_theme(colors.primary.500)]",
          "hover:[--border-color:_theme(colors.primary.600)]",
          "focus-visible:[--border-color:_theme(colors.primary.600)]",
          "active:[--border-color:_theme(colors.primary.700)]",
          "dark:[--border-color:_theme(colors.primary.400)]",
          "dark:hover:[--border-color:_theme(colors.primary.500)]",
          "dark:focus-visible:[--border-color:_theme(colors.primary.500)]",
          "dark:active:[--border-color:_theme(colors.primary.600)]",
        ],
        danger: [
          "[--border-color:_theme(colors.red.500)]",
          "hover:[--border-color:_theme(colors.red.600)]",
          "focus-visible:[--border-color:_theme(colors.red.600)]",
          "active:[--border-color:_theme(colors.red.700)]",
          "dark:[--border-color:_theme(colors.red.400)]",
          "dark:hover:[--border-color:_theme(colors.red.500)]",
          "dark:focus-visible:[--border-color:_theme(colors.red.500)]",
          "dark:active:[--border-color:_theme(colors.red.600)]",
        ],
        link: [
          "[--border-color:_theme(colors.blue.500)]",
          "hover:[--border-color:_theme(colors.blue.600)]",
          "focus-visible:[--border-color:_theme(colors.blue.600)]",
          "active:[--border-color:_theme(colors.blue.700)]",
          "dark:[--border-color:_theme(colors.blue.400)]",
          "dark:hover:[--border-color:_theme(colors.blue.500)]",
          "dark:focus-visible:[--border-color:_theme(colors.blue.500)]",
          "dark:active:[--border-color:_theme(colors.blue.600)]",
        ],
        successful: [
          "[--border-color:_theme(colors.green.500)]",
          "hover:[--border-color:_theme(colors.green.600)]",
          "focus-visible:[--border-color:_theme(colors.green.600)]",
          "active:[--border-color:_theme(colors.green.700)]",
          "dark:[--border-color:_theme(colors.green.400)]",
          "dark:hover:[--border-color:_theme(colors.green.500)]",
          "dark:focus-visible:[--border-color:_theme(colors.green.500)]",
          "dark:active:[--border-color:_theme(colors.green.600)]",
        ],
        warning: [
          "[--border-color:_theme(colors.yellow.500)]",
          "hover:[--border-color:_theme(colors.yellow.600)]",
          "focus-visible:[--border-color:_theme(colors.yellow.600)]",
          "active:[--border-color:_theme(colors.yellow.700)]",
          "dark:[--border-color:_theme(colors.yellow.400)]",
          "dark:hover:[--border-color:_theme(colors.yellow.500)]",
          "dark:focus-visible:[--border-color:_theme(colors.yellow.500)]",
          "dark:active:[--border-color:_theme(colors.yellow.600)]",
        ],
      },
    },
    defaultVariants: {
      variant: "subtle",
      color: "theme",
      compiledVariant: "theme-ghost",
    },
  }
);

const compileVariants = ({
  color,
  compact,
  variant,
  ...others
}: VariantProps<typeof buttonClassName>) => {
  let replacedVariant = null;
  switch (variant) {
    case "filled": {
      replacedVariant = "filled";
      break;
    }
    case "ghost": {
      replacedVariant = "ghost";
      break;
    }
    case "underline": {
      replacedVariant = "ghost";
      break;
    }
    default: {
      replacedVariant = "subtle";
      break;
    }
  }

  const generatedVariant = `${color}-${replacedVariant}` as VariantProps<
    typeof buttonClassName
  >["compiledVariant"];

  return {
    color,
    compact,
    variant,
    compiledVariant: generatedVariant,
    ...others,
  };
};

interface BaseButtonProps<T extends ElementType>
  extends Omit<VariantProps<typeof buttonClassName>, "compiledVariant"> {
  component?: T | undefined;
  iconRight?: ReactNode;
  iconLeft?: ReactNode;
  compact?: boolean;
  className?: string;
  square?: boolean;
  loading?: boolean;
}

export type ButtonProps<T extends ElementType> = BaseButtonProps<T> &
  Omit<ComponentProps<T>, keyof BaseButtonProps<T>>;

/**
 * Button Element
 * @param {ButtonProps} props
 * @returns {JSX.Element}
 * @example
 */
export const Button = <T extends ElementType = "button">({
  children,
  variant = "subtle",
  color = "theme",
  iconLeft,
  iconRight,
  compact = false,
  square = false,
  className,
  component,
  loading,
  disabled,
  onClick,
  ...props
}: ButtonProps<T>): JSX.Element =>
  createElement(
    component ? component : "button",
    {
      role: "button",
      onClick: !loading || !disabled ? onClick : undefined,
      disabled: loading || disabled,
      ["data-loading"]: loading,
      className: twMerge(
        buttonClassName(
          compileVariants({
            color,
            compact,
            square,
            variant,
          })
        ),
        className
      ),
      ...props,
    },
    <Fragment>
      {loading && (
        <Loading
          data-loading
          className="absolute left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] w-4 h-4"
        />
      )}
      {iconLeft && (
        <span className="block whitespace-nowrap w-fit h-full">{iconLeft}</span>
      )}
      <span
        data-loading={loading}
        className="block whitespace-nowrap w-fit h-full transition-opacity data-[loading=true]:opacity-0"
      >
        {children}
      </span>
      {iconRight && (
        <span className="block whitespace-nowrap w-fit h-full">
          {iconRight}
        </span>
      )}
    </Fragment>
  );

Loading...