Icon Button

Anatomy

States

Default

Hover

Pressed

Focus

Pending

Disabled

Icons

Copy

Next

Retry

Confirm

Themes

Light

Dark

Primary override

Example

View code
"use client";

import { IconButton } 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 CopyIconAction() {
  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 (
    <>
      <IconButton
        aria-label="Copy source"
        icon={
          operationVisual.visualPhase === "pending"
            ? "pending"
            : operationVisual.visualPhase === "success"
              ? "success"
              : operationVisual.visualPhase === "error"
                ? "retry"
                : "copy"
        }
        onClick={copyExample}
        pending={operation.phase === "pending"}
      />
      <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>
    </>
  );
}