Button

Anatomy

Calibration

Metrics

Rendered height
00px
Minimum inline × block
00px × 00px
Padding block / inline
00px / 00px
Label size / leading
00px / 00px
Label weight
000
Tracking
000000
Radius
00px
Icon box inline × block
00px × 00px
SVG stroke value
0.00px computed
Icon–label gap
00px
Refresh
Load + button resize

Target

40 reference

44 selected

States

Default

Hover

Pressed

Focus

Pending

Disabled

Variations

Label only

Leading icon

Trailing icon

Themes

Light

Dark

Primary override

Content

Examples

Failure → retry

Sustained pending

Capture: 0 · Bubble: 0

View code
"use client";

import { Button } from "@baseline/ui";
import { useEffect, useRef, useState } from "react";
import { flushSync } from "react-dom";

type CopyOperation = {
  id: number;
  phase: "idle" | "pending" | "success" | "error";
};

const pendingVisualDelay = 160;

function useDelayedOperationVisual() {
  const timer = useRef<number | null>(null);
  const [visualPhase, setVisualPhase] = useState<CopyOperation["phase"]>("idle");

  useEffect(() => () => {
    if (timer.current !== null) window.clearTimeout(timer.current);
  }, []);

  function begin() {
    if (timer.current !== null) window.clearTimeout(timer.current);
    timer.current = window.setTimeout(() => {
      timer.current = null;
      setVisualPhase("pending");
    }, pendingVisualDelay);
  }

  function settle(phase: "idle" | "success" | "error") {
    if (timer.current !== null) window.clearTimeout(timer.current);
    timer.current = null;
    setVisualPhase(phase);
  }

  return { begin, settle, visualPhase };
}

export function CopyAction() {
  const nextOperationId = useRef(0);
  const [operation, setOperation] = useState<CopyOperation>({ id: 0, phase: "idle" });
  const operationVisual = useDelayedOperationVisual();

  async function copyExample() {
    const id = ++nextOperationId.current;
    flushSync(() => setOperation({ id, phase: "pending" }));
    operationVisual.begin();

    try {
      await navigator.clipboard.writeText("Complete example");
      setOperation({ id, phase: "success" });
      operationVisual.settle("success");
    } catch {
      setOperation({ id, phase: "error" });
      operationVisual.settle("error");
    }
  }

  const status =
    operation.phase === "pending"
      ? "Copying…"
      : operation.phase === "success"
        ? "Copied to clipboard."
        : operation.phase === "error"
          ? "Clipboard unavailable. Try again."
          : "";

  return (
    <>
      <Button
        leadingIcon={
          operationVisual.visualPhase === "pending"
            ? "pending"
            : operationVisual.visualPhase === "success"
              ? "success"
              : operationVisual.visualPhase === "error"
                ? "retry"
                : "copy"
        }
        onClick={copyExample}
        pending={operation.phase === "pending"}
      >
        Copy example
      </Button>
      <span
        aria-live="polite"
        className="bl-visually-hidden"
        data-operation-id={operation.id}
        data-phase={operation.phase}
        role="status"
      >
        {operation.id > 0 ? (
          <span key={operation.id + "-" + operation.phase}>{status}</span>
        ) : null}
      </span>
    </>
  );
}