Skip to content
Components

Button

Triggers an action with visual variants for emphasis and context.

Playground

material

Sets the button's visual emphasis.

size

Sets the control height and horizontal padding.

scheme

Applies a semantic color scheme.

shape

Uses a text button or a square icon button.

loading

Shows progress while preserving the button label and width.

disabled

Prevents interaction with the native button.

static

Removes tactile scale feedback for motion-sensitive contexts.

Usage

tsx
import { Button } from "earthling-ui/button";

export function Example() {
  return (
    <>
      <Button type="button">Create project</Button>
    </>
  );
}
  • Use a short verb phrase that describes the action.
  • The loading state keeps the label width stable, sets aria-busy, and disables the native button.
  • Icon-only buttons still need an accessible name through aria-label.

Installation

Start with the project setup for React and Tailwind CSS 4. Both paths use the same component and theme.

bash
npm install earthling-ui

Use the component import shown above. Package updates keep the implementation current.

API reference

Declarations from the current library build, including inherited primitive props. Playground controls above show selected options; they are not the complete API.

tsx
// Generated by dts-bundle-generator v9.5.1

import { VariantProps } from 'class-variance-authority';
import { ComponentProps } from 'react';

export declare const buttonVariants: (props?: ({
	material?: "paper" | "outline" | "ghost" | null | undefined;
	scheme?: "default" | "primary" | "secondary" | "tertiary" | "neutral" | "muted" | "good" | "caution" | "bad" | null | undefined;
	size?: "sm" | "md" | "lg" | null | undefined;
	shape?: "pill" | "icon" | null | undefined;
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
export interface ButtonProps extends ComponentProps<"button">, VariantProps<typeof buttonVariants> {
	asChild?: boolean;
	loading?: boolean;
	/** Disables the tactile scale-on-press feedback when motion would distract. */
	static?: boolean;
}
export declare const Button: import("react").ForwardRefExoticComponent<Omit<ButtonProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;

export {};

Runtime dependencies: @radix-ui/react-slot, class-variance-authority, cnfast.

Source

Use the eject command to copy this implementation with its required helpers and project-specific imports.

View Button implementation
tsx
"use client";

import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/cn";
import { schemes } from "@/utils/variants";
import { type ComponentProps, forwardRef } from "react";

const buttonVariants = cva(
  "relative inline-flex cursor-pointer items-center justify-center gap-2 rounded-(--radius-control) border border-transparent text-sm font-medium whitespace-nowrap ring-offset-background focus-visible:ring-2 focus-visible:ring-outline focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-bad aria-invalid:ring-bad/30",
  {
    variants: {
      material: {
        paper:
          "bg-(--scheme-tint) text-(--scheme-foreground) shadow-xs hover:bg-(--scheme-tint)/85 aria-pressed:bg-(--scheme-tint)/70 aria-pressed:hover:bg-(--scheme-tint)/55",
        outline:
          "text-foreground border-(--scheme-tint)/30 hover:border-(--scheme-tint)/50 hover:bg-(--scheme-tint)/5 aria-pressed:bg-(--scheme-tint)/10 aria-pressed:hover:bg-(--scheme-tint)/15",
        ghost:
          "text-foreground hover:bg-(--scheme-tint)/5 aria-pressed:bg-(--scheme-tint)/10 aria-pressed:hover:bg-(--scheme-tint)/15",
      },
      scheme: schemes,
      size: { sm: "h-9 px-3", md: "h-10 px-4 py-2", lg: "h-11 px-8" },
      shape: { pill: "", icon: "px-0 aspect-square" },
    },
    defaultVariants: {
      material: "paper",
      scheme: "primary",
      size: "md",
      shape: "pill",
    },
  },
);

export interface ButtonProps
  extends ComponentProps<"button">, VariantProps<typeof buttonVariants> {
  asChild?: boolean;
  loading?: boolean;
  /** Disables the tactile scale-on-press feedback when motion would distract. */
  static?: boolean;
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      material,
      size,
      scheme,
      shape,
      asChild = false,
      loading = false,
      static: isStatic = false,
      disabled,
      children,
      ...props
    },
    ref,
  ) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(
          buttonVariants({ material, size, scheme, shape }),
          !isStatic &&
            "transition-transform duration-150 ease-out motion-reduce:transition-none active:scale-[0.96] motion-reduce:active:scale-100",
          className,
        )}
        ref={ref}
        data-scheme={scheme}
        disabled={asChild ? undefined : disabled || loading}
        aria-disabled={disabled || loading || undefined}
        aria-busy={loading || undefined}
        {...props}
      >
        {loading && !asChild ? (
          <>
            <span className="inline-flex items-center gap-[inherit] opacity-0">
              {children}
            </span>
            <span
              aria-hidden="true"
              className="absolute size-4 animate-spin rounded-full border-2 border-current/30 border-t-current motion-reduce:animate-none"
            />
          </>
        ) : (
          children
        )}
      </Comp>
    );
  },
);
Button.displayName = "Button";

export { Button, buttonVariants };