Skip to content
Andrew SmithSolutions
← Component library

Button

Actions

The three-weight button set — a solid primary, an outlined secondary and the clay accent — sized to sit on a line of text.

ActionLinkFocus ringCSS only — no dependency

Preview

Controls

Notes

Three variants is the whole set on purpose. A fourth almost always means the page is asking the reader to make a decision it should have made for them.

The outline variant borders on --control-border, not --rule. Rules are decorative hairlines; anything you can click owes 3:1 contrast against the page under WCAG SC 1.4.11, and --rule does not clear it.

There is no disabled-with-tooltip pattern here. A disabled control that will not say why it is disabled is a dead end — prefer leaving it enabled and explaining the failure after the click.

Source

Show
import { cn } from "@/lib/cn";

const base =
  "inline-flex items-center justify-center gap-2 rounded-full font-medium transition-all disabled:pointer-events-none disabled:opacity-45";

const variants = {
  primary: "bg-ink text-paper hover:opacity-85",
  // Borders on --control-border, not --rule: anything clickable owes 3:1
  // against the page (SC 1.4.11) and the decorative hairline does not clear it.
  secondary: "border border-control-border text-ink hover:border-ink hover:bg-paper-sunken",
  accent: "bg-accent text-on-accent hover:bg-accent-hover",
} as const;

const sizes = {
  sm: "px-4 py-2 text-[13px]",
  md: "px-5 py-2.5 text-sm",
  lg: "px-6 py-3 text-base",
} as const;

export type ButtonVariant = keyof typeof variants;
export type ButtonSize = keyof typeof sizes;

export function Button({
  variant = "primary",
  size = "md",
  className,
  children,
  ...rest
}: {
  variant?: ButtonVariant;
  size?: ButtonSize;
} & React.ComponentProps<"button">) {
  return (
    <button className={cn(base, variants[variant], sizes[size], className)} {...rest}>
      {children}
    </button>
  );
}

/** Trailing arrow. Decorative — the label already says where you are going. */
export function Arrow({ className }: { className?: string }) {
  return (
    <svg
      width="14"
      height="14"
      viewBox="0 0 14 14"
      fill="none"
      aria-hidden="true"
      className={className}
    >
      <path
        d="M3 11L11 3M11 3H5M11 3v6"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export default function Demo({
  variant = "primary",
  size = "md",
  label = "Start a conversation",
  arrow = true,
  disabled = false,
}: {
  variant?: ButtonVariant;
  size?: ButtonSize;
  label?: string;
  arrow?: boolean;
  disabled?: boolean;
}) {
  return (
    <Button variant={variant} size={size} disabled={disabled}>
      {label}
      {arrow && <Arrow />}
    </Button>
  );
}

Next

Field

Next step

Tell me where the business is losing time.

No pitch deck and no discovery-call funnel. Describe the bottleneck and I will tell you plainly whether it is worth building something for, and roughly what that would take.