Skip to content

Button

Compact button with 6 variants (indigo primary with inner highlight, ink secondary, outline, ghost, danger, link), 4 sizes, icons, keyboard-shortcut hint, loading state and link mode.

minimal/ui/button
Open ↗

Source

import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
import { cn } from "@/lib/utils";

export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger" | "link";
export type ButtonSize = "sm" | "md" | "lg" | "icon";

interface BaseProps {
  variant?: ButtonVariant;
  size?: ButtonSize;
  leftIcon?: ReactNode;
  rightIcon?: ReactNode;
  /** Keyboard hint rendered at the end, e.g. "⌘↵". Decorative only. */
  shortcut?: string;
  loading?: boolean;
  className?: string;
  children?: ReactNode;
}

export type ButtonProps =
  | (BaseProps & Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps> & { href?: undefined })
  | (BaseProps & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof BaseProps> & { href: string });

const VARIANTS: Record<ButtonVariant, string> = {
  primary: "bg-da-primary text-da-primary-fg hover:bg-da-primary/90 [box-shadow:inset_0_1px_0_rgb(255_255_255/0.12),var(--da-shadow-sm)]",
  secondary: "bg-da-fg text-da-bg hover:bg-da-fg/85",
  outline: "da-stroke bg-da-surface text-da-surface-fg shadow-da-sm hover:bg-da-muted",
  ghost: "text-da-muted-fg hover:bg-da-fg/5 hover:text-da-fg",
  danger: "bg-da-danger text-da-danger-fg shadow-da-sm hover:bg-da-danger/90",
  link: "h-auto px-0 text-da-fg underline-offset-4 hover:underline",
};

const SIZES: Record<ButtonSize, string> = {
  sm: "h-7 gap-1.5 rounded-da-sm px-2.5 text-[13px] [&_svg]:size-3.5",
  md: "h-9 gap-2 rounded-da-md px-3.5 text-sm [&_svg]:size-4",
  lg: "h-11 gap-2 rounded-da-md px-5 text-[15px] [&_svg]:size-4",
  icon: "size-9 rounded-da-md [&_svg]:size-4",
};

export function buttonClasses({ variant = "primary", size = "md", className }: { variant?: ButtonVariant; size?: ButtonSize; className?: string } = {}): string {
  return cn(
    "da-focus da-transition inline-flex shrink-0 cursor-pointer items-center justify-center font-medium whitespace-nowrap select-none active:scale-[0.98]",
    "disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
    SIZES[size],
    VARIANTS[variant],
    className,
  );
}

function Spinner() {
  return (
    <svg aria-hidden viewBox="0 0 16 16" fill="none" className="animate-spin motion-reduce:animate-none">
      <circle cx="8" cy="8" r="6" stroke="currentColor" strokeOpacity="0.25" strokeWidth="2" />
      <path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
    </svg>
  );
}

export function Button(props: ButtonProps) {
  const { variant, size, leftIcon, rightIcon, shortcut, loading = false, className, children, ...rest } = props;
  const classes = buttonClasses({ variant, size, className });
  const content = (
    <>
      {loading ? <Spinner /> : leftIcon}
      {children}
      {rightIcon}
      {shortcut && (
        <kbd aria-hidden className="-mr-1 ml-0.5 rounded-[4px] bg-current/15 px-1 font-da-mono text-[10px] leading-4 opacity-80">
          {shortcut}
        </kbd>
      )}
    </>
  );

  if (rest.href !== undefined) {
    return (
      <a {...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)} className={classes} aria-disabled={loading || undefined}>
        {content}
      </a>
    );
  }

  const { type = "button", disabled, ...buttonProps } = rest as ButtonHTMLAttributes<HTMLButtonElement>;
  return (
    <button {...buttonProps} type={type} disabled={disabled || loading} aria-busy={loading || undefined} className={classes}>
      {content}
    </button>
  );
}

export default Button;

modules/minimal/ui/button/index.tsx

Props

PropTypeDefaultDescription
variant"primary" | "secondary" | "outline" | "ghost" | "danger" | "link""primary"Visual style.
size"sm" | "md" | "lg" | "icon""md"Size preset; `icon` is square — pass aria-label.
hrefstring—Renders an <a> instead of a <button>.
leftIcon / rightIconReactNode—Icons around the label (left icon becomes a spinner while loading).
shortcutstring—Decorative keyboard hint, e.g. "⌘↵".
loadingbooleanfalseSpinner + disabled + aria-busy.
...restButtonHTMLAttributes | AnchorHTMLAttributes—Native attributes.

Other button variants in Minimal

Button in other art directions

Button

Pressable button with 6 variants (primary, secondary, ink, outline, ghost, danger), 4 sizes, icons, loading state and link mode (href). Hard shadow collapses on hover/press.

BrutalistButton

Copy Button

Code field with an attached yellow copy button that flips to green 'Copied' with a check; optional secret masking for API keys and custom display text for commands.

BrutalistButton

Segmented Control

Ruled segmented control for mutually exclusive options: mono uppercase segments split by strokes, ink selected segment, yellow hover. Controlled or uncontrolled, typed values.

BrutalistButton

Split Button

Yellow primary action glued to a chevron trigger that opens a ruled dropdown of secondary actions with descriptions (Radix DropdownMenu). Both halves flip to ink on hover.

BrutalistButton

Button

Pill button with 5 variants (primary→accent gradient, frosted glass, solid ink, ghost, danger), 4 sizes, icons, loading state and link mode. Lifts on hover with a slight overshoot.

GlassButton

Icon Button

Round frosted icon button with a tooltip; optional toggle mode (aria-pressed) that swaps the icon and fills with the gradient or danger color — ideal for call controls.

GlassButton

Record Button

Record toggle pill: red dot inside a soft halo that morphs into a stop square with a pulsing ring and a running mm:ss timer while recording.

GlassButton

Shine Button

Gradient pill button with a glossy top highlight and a bright light sweep crossing it on hover or keyboard focus (CSS only); renders an anchor when href is set. Two sizes.

GlassButton

Arrow Link

Text link with an ↗ that nudges up-right on hover and an ink underline that wipes in from the left; optional mono index prefix; scales from inline to display size.

Mono CleanButton

Button

Square monochrome button: ink primary, outlined (inverts on hover), ghost and underline (signal-colored underline) variants; three sizes and an optional trailing glyph.

Mono CleanButton

Icon Button

Square icon-only button with a hairline border (darkens on hover), ghost or solid ink variant; required label for aria-label and title.

Mono CleanButton

Text Toggle

Typographic toggle: options are plain mono words separated by slashes; the active one is ink with a signal underline, others muted — radio semantics with arrow keys.

Mono CleanButton

Button

Corporate button: 6px radius, five variants (blue primary, bordered white secondary, ghost, danger, link), three sizes, leading/trailing icons.

Neo CorporateButton

Button Group

Attached segmented button group with shared borders (radio semantics) — for period pickers and view switchers. Selected segment gets a surface-2 fill and bold label.

Neo CorporateButton

Icon Button

Square icon-only button (secondary, ghost, primary, danger) with required aria-label, title tooltip and optional count/dot badge.

Neo CorporateButton

Loading Button

Async action button: idle → spinner + “Processing…” (aria-busy, width locked) → green check “Sent” for 2s → idle. Default simulates a 1.4s payment run.

Neo CorporateButton

Arrow Button

Link-button with a round arrow badge at its end: on hover the badge grows and the arrow turns from ↗ to →, a signature call-to-action for the DA.

Organic SoftButton

Button

Soft pill button: sage primary, sage-tint soft, terracotta clay, outlined ink and ghost variants; three sizes, icons, gentle hover.

Organic SoftButton

Icon Button

Round icon-only button in soft sage, sage, cream (with shadow) or ghost; required label used for aria-label and title.

Organic SoftButton

Segmented

Pill segmented control on a sand track: the selected pill is cream with a soft shadow and slides with a sliding indicator; radio semantics with arrow keys.

Organic SoftButton

Button

Friendly pill button: six variants (periwinkle primary, soft lavender, outline, ghost, danger, ink), three sizes, left/right icons, loading spinner and link mode, with a tiny bounce on hover.

Soft FlatButton

Fab

Floating action button: a big round periwinkle “+” that rotates into “×” and fans out labelled pastel speed-dial actions with a staggered bounce; closes on Escape or outside click.

Soft FlatButton

Icon Button

Round pastel icon button in six tones (neutral, peach, mint, lavender, butter, primary) and three sizes, with optional red counter bubble and a springy press.

Soft FlatButton

Toggle Chips

Row of pill toggle chips with emoji; selected chips fill periwinkle and show a popping check. Multiple or single selection, controlled or not.

Soft FlatButton