Skip to content

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-flat/ui/button
Open ↗

Source

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

type Variant = "primary" | "soft" | "outline" | "ghost" | "danger" | "ink";
type Size = "sm" | "md" | "lg";

type Common = {
  variant?: Variant;
  size?: Size;
  loading?: boolean;
  iconLeft?: ReactNode;
  iconRight?: ReactNode;
  children?: ReactNode;
  className?: string;
};

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

const VARIANTS: Record<Variant, string> = {
  primary: "bg-da-primary text-da-primary-fg shadow-da-sm hover:shadow-da-md",
  soft: "bg-da-primary/12 text-da-fg hover:bg-da-primary/20",
  outline: "border-2 border-da-border-strong bg-da-surface text-da-fg hover:border-da-primary",
  ghost: "text-da-fg hover:bg-da-surface-2",
  danger: "bg-da-danger text-da-danger-fg shadow-da-sm hover:shadow-da-md",
  ink: "bg-da-fg text-da-bg hover:shadow-da-md",
};
const SIZES: Record<Size, string> = {
  sm: "h-9 px-4 text-sm gap-1.5 [&_svg]:size-4",
  md: "h-11 px-5 text-[15px] gap-2 [&_svg]:size-4",
  lg: "h-14 px-7 text-base gap-2 [&_svg]:size-5",
};

/** Friendly pill button: 6 variants (primary, soft lavender, outline, ghost, danger, ink), 3 sizes, icons, loading state, link mode; tiny bounce on hover. */
export function Button({ variant = "primary", size = "md", loading = false, iconLeft, iconRight, children = "Add task", className, ...rest }: ButtonProps) {
  const cls = cn(
    "da-focus inline-flex items-center justify-center rounded-da-pill font-semibold whitespace-nowrap transition-[translate,box-shadow,background-color,border-color] duration-(--da-duration) ease-da-emphasized hover:-translate-y-0.5 active:translate-y-0 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
    VARIANTS[variant],
    SIZES[size],
    className,
  );
  const inner = (
    <>
      {loading ? <Loader2 aria-hidden className="animate-spin motion-reduce:animate-none" /> : iconLeft}
      {children}
      {!loading && iconRight}
    </>
  );
  if ("href" in rest && rest.href !== undefined) {
    return (
      <a {...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)} aria-disabled={loading || undefined} className={cls}>
        {inner}
      </a>
    );
  }
  const b = rest as ButtonHTMLAttributes<HTMLButtonElement>;
  return (
    <button type="button" {...b} disabled={b.disabled || loading} aria-busy={loading || undefined} className={cls}>
      {inner}
    </button>
  );
}

export default Button;

modules/soft-flat/ui/button/index.tsx

Props

PropTypeDefaultDescription
variant"primary" | "soft" | "outline" | "ghost" | "danger" | "ink""primary"Style.
size"sm" | "md" | "lg""md"Size.
loadingboolean—Spinner + disabled.
iconLeft / iconRightReactNode—Icons.
hrefstring—Render as link.
...restbutton / anchor attributes—Forwarded.

Other button variants in Soft Flat

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

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.

MinimalButton

Loading Button

Async action button that morphs through pending (spinner), success (green check) and error (red cross) states, then resets; all labels share one grid cell so the width never jumps.

MinimalButton

Shortcut Button

Button displaying its keyboard shortcut as keycaps; pressing the shortcut anywhere on the page clicks it (ignored while typing in a field). Primary, secondary and ghost styles.

MinimalButton

Toggle Group

Single-choice segmented toolbar on a muted track with a raised active segment (Radix ToggleGroup): text, icon-only or mixed options, two sizes, controlled or uncontrolled.

MinimalButton

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