Web InnoventixFreeCode

Icon Only Button

Original · free

Square and circular icon-only buttons with tooltips and aria-labels.

byWeb InnoventixReact + Tailwind
btnicononlybuttons
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/btn-icon-only.json
btn-icon-only.tsx
"use client";

import { useState, useRef, useEffect, useId, type ReactNode } from "react";
import { useReducedMotion } from "motion/react";

type Shape = "square" | "circle";
type Size = "sm" | "md" | "lg";

const SIZE_MAP: Record<Size, { box: string; icon: string; radius: string }> = {
  sm: { box: "h-8 w-8", icon: "h-4 w-4", radius: "rounded-lg" },
  md: { box: "h-10 w-10", icon: "h-[1.15rem] w-[1.15rem]", radius: "rounded-xl" },
  lg: { box: "h-12 w-12", icon: "h-5 w-5", radius: "rounded-2xl" },
};

type IconProps = { className?: string };

function IconHeart({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
      <path d="M12 21s-7.5-4.35-10.05-8.4C.36 9.98.9 6.9 3.3 5.55c1.86-1.05 4.2-.6 5.55.99L12 9.9l3.15-3.36c1.35-1.59 3.69-2.04 5.55-.99 2.4 1.35 2.94 4.43 1.35 7.05C19.5 16.65 12 21 12 21z" />
    </svg>
  );
}

function IconHeartOutline({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M12 20.2s-6.7-3.9-9-7.5C1.5 10.4 2 7.7 4.1 6.5c1.7-.95 3.8-.55 5 .9L12 10.4l2.9-3c1.2-1.45 3.3-1.85 5-.9 2.1 1.2 2.6 3.9 1.1 6.2-2.3 3.6-9 7.5-9 7.5z" />
    </svg>
  );
}

function IconBookmark({ className, filled }: IconProps & { filled: boolean }) {
  return (
    <svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M6 4h12a1 1 0 0 1 1 1v15l-7-4-7 4V5a1 1 0 0 1 1-1z" />
    </svg>
  );
}

function IconCopy({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <rect x="9" y="9" width="11" height="11" rx="2" />
      <path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1" />
    </svg>
  );
}

function IconCheck({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M20 6 9 17l-5-5" />
    </svg>
  );
}

function IconSun({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <circle cx="12" cy="12" r="4" />
      <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
    </svg>
  );
}

function IconMoon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
    </svg>
  );
}

function IconMute({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M11 5 6 9H3v6h3l5 4z" />
      <path d="M22 9l-6 6M16 9l6 6" />
    </svg>
  );
}

function IconVolume({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M11 5 6 9H3v6h3l5 4z" />
      <path d="M16 8.5a5 5 0 0 1 0 7M19 6a9 9 0 0 1 0 12" />
    </svg>
  );
}

function IconSettings({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <circle cx="12" cy="12" r="3" />
      <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
    </svg>
  );
}

function IconTrash({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6M10 11v6M14 11v6" />
    </svg>
  );
}

function IconSpinner({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" aria-hidden="true" className={className}>
      <path d="M12 3a9 9 0 1 0 9 9" />
    </svg>
  );
}

function IconGithub({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
      <path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.85 9.73.5.1.68-.22.68-.49 0-.24-.01-.87-.01-1.71-2.78.62-3.37-1.37-3.37-1.37-.46-1.18-1.11-1.49-1.11-1.49-.91-.63.07-.62.07-.62 1 .07 1.53 1.05 1.53 1.05.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.36-2.22-.26-4.56-1.14-4.56-5.05 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.27 2.75 1.05A9.35 9.35 0 0 1 12 6.84c.85 0 1.71.12 2.51.34 1.91-1.32 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.92-2.34 4.78-4.57 5.03.36.32.68.94.68 1.9 0 1.37-.01 2.48-.01 2.82 0 .27.18.6.69.49A10.02 10.02 0 0 0 22 12.25C22 6.58 17.52 2 12 2z" />
    </svg>
  );
}

function IconX({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
      <path d="M18.24 2.25h3.31l-7.23 8.26 8.5 11.24h-6.66l-5.22-6.82-5.97 6.82H1.66l7.73-8.84L1.24 2.25h6.83l4.71 6.23 5.46-6.23zm-1.16 17.52h1.83L7.01 4.13H5.05l12.03 15.64z" />
    </svg>
  );
}

function IconLinkedin({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
      <path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.63-1.85 3.36-1.85 3.59 0 4.25 2.36 4.25 5.44v6.3zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0z" />
    </svg>
  );
}

type Tone = "neutral" | "indigo" | "rose" | "emerald";

const TONE_RING: Record<Tone, string> = {
  neutral: "focus-visible:ring-slate-400 dark:focus-visible:ring-slate-500",
  indigo: "focus-visible:ring-indigo-500",
  rose: "focus-visible:ring-rose-500",
  emerald: "focus-visible:ring-emerald-500",
};

function baseClasses(shape: Shape, size: Size) {
  const s = SIZE_MAP[size];
  const radius = shape === "circle" ? "rounded-full" : s.radius;
  return `group relative inline-flex ${s.box} items-center justify-center ${radius} select-none outline-none transition-[transform,background-color,border-color,box-shadow,color] duration-200 ease-out focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 active:translate-y-px disabled:pointer-events-none disabled:opacity-40`;
}

type TooltipButtonProps = {
  label: string;
  shape: Shape;
  size: Size;
  tone?: Tone;
  variant?: "solid" | "soft" | "ghost" | "outline";
  onClick?: () => void;
  disabled?: boolean;
  pressed?: boolean;
  busy?: boolean;
  children: ReactNode;
};

function TooltipButton({
  label,
  shape,
  size,
  tone = "neutral",
  variant = "soft",
  onClick,
  disabled,
  pressed,
  busy,
  children,
}: TooltipButtonProps) {
  const tipId = useId();

  const variantClasses: Record<string, string> = {
    solid:
      "bg-slate-900 text-white shadow-sm hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white",
    soft:
      "bg-slate-100 text-slate-700 hover:bg-slate-200 hover:text-slate-900 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700 dark:hover:text-white",
    ghost:
      "text-slate-500 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white",
    outline:
      "border border-slate-300 bg-white text-slate-700 hover:border-slate-400 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-slate-600 dark:hover:bg-slate-800",
  };

  return (
    <button
      type="button"
      aria-label={label}
      aria-describedby={tipId}
      aria-pressed={typeof pressed === "boolean" ? pressed : undefined}
      aria-busy={busy || undefined}
      disabled={disabled || busy}
      onClick={onClick}
      className={`${baseClasses(shape, size)} ${variantClasses[variant]} ${TONE_RING[tone]} hover:-translate-y-0.5`}
    >
      {children}
      <span
        role="tooltip"
        id={tipId}
        className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 -translate-x-1/2 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
      >
        {label}
        <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
      </span>
    </button>
  );
}

function Row({ label, children }: { label: string; children: ReactNode }) {
  return (
    <div className="flex flex-col gap-3">
      <span className="text-[0.7rem] font-semibold uppercase tracking-[0.14em] text-slate-400 dark:text-slate-500">
        {label}
      </span>
      <div className="flex flex-wrap items-center gap-3">{children}</div>
    </div>
  );
}

export default function BtnIconOnly() {
  const reduceMotion = useReducedMotion();

  const [liked, setLiked] = useState(false);
  const [saved, setSaved] = useState(true);
  const [muted, setMuted] = useState(false);
  const [dark, setDark] = useState(false);
  const [copied, setCopied] = useState(false);
  const [loading, setLoading] = useState(false);

  const copyTimer = useRef<number | null>(null);
  const loadTimer = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (copyTimer.current) window.clearTimeout(copyTimer.current);
      if (loadTimer.current) window.clearTimeout(loadTimer.current);
    };
  }, []);

  async function handleCopy() {
    try {
      await navigator.clipboard.writeText("npm i @webinnoventix/ui");
    } catch {
      /* clipboard unavailable — still surface success state */
    }
    setCopied(true);
    if (copyTimer.current) window.clearTimeout(copyTimer.current);
    copyTimer.current = window.setTimeout(() => setCopied(false), 1600);
  }

  function handleLoad() {
    if (loading) return;
    setLoading(true);
    if (loadTimer.current) window.clearTimeout(loadTimer.current);
    loadTimer.current = window.setTimeout(() => setLoading(false), 1800);
  }

  const spin = reduceMotion ? "" : "btnio-spin";

  return (
    <section className="relative w-full bg-white px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-10">
      <style>{`
        .btnio-tip {
          transform: translate(-50%, 4px);
          transition: opacity .18s ease, transform .18s ease;
        }
        .group:hover .btnio-tip,
        .group:focus-visible .btnio-tip {
          opacity: 1;
          transform: translate(-50%, 0);
        }
        @keyframes btnio-spin { to { transform: rotate(360deg); } }
        .btnio-spin { animation: btnio-spin .8s linear infinite; transform-origin: 50% 50%; }
        @keyframes btnio-pop {
          0% { transform: scale(1); }
          40% { transform: scale(1.28); }
          100% { transform: scale(1); }
        }
        .btnio-pop { animation: btnio-pop .32s ease-out; }
        @media (prefers-reduced-motion: reduce) {
          .btnio-tip { transition: opacity .18s ease; transform: translate(-50%, 0); }
          .btnio-spin { animation: none; }
          .btnio-pop { animation: none; }
        }
      `}</style>

      <div className="mx-auto max-w-4xl">
        <header className="mb-14 max-w-xl">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            FreeCode / Buttons
          </span>
          <h2 className="mt-5 text-3xl font-semibold tracking-tight sm:text-4xl">
            Icon-only buttons
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-500 dark:text-slate-400">
            Square and circular controls with hover tooltips and proper{" "}
            <code className="rounded bg-slate-100 px-1.5 py-0.5 text-[0.85em] text-slate-700 dark:bg-slate-800 dark:text-slate-300">
              aria-label
            </code>
            s. Focus rings, pressed states and live feedback all wired up.
          </p>
        </header>

        <div className="grid grid-cols-1 gap-10 rounded-3xl border border-slate-200 bg-slate-50/60 p-8 dark:border-slate-800 dark:bg-slate-900/40 sm:p-10 md:grid-cols-2">
          <Row label="Square · variants">
            <TooltipButton label="Settings" shape="square" size="md" variant="solid">
              <IconSettings className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="Copy install command" shape="square" size="md" variant="soft">
              <IconCopy className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="Open menu" shape="square" size="md" variant="outline">
              <IconSettings className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="More options" shape="square" size="md" variant="ghost">
              <IconCopy className={SIZE_MAP.md.icon} />
            </TooltipButton>
          </Row>

          <Row label="Circle · variants">
            <TooltipButton label="Confirm" shape="circle" size="md" tone="emerald" variant="solid">
              <IconCheck className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="Notifications" shape="circle" size="md" variant="soft">
              <IconVolume className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="Profile" shape="circle" size="md" variant="outline">
              <IconSettings className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="Dismiss" shape="circle" size="md" variant="ghost">
              <IconX className={SIZE_MAP.md.icon} />
            </TooltipButton>
          </Row>

          <Row label="Sizes · sm / md / lg">
            <TooltipButton label="Settings (small)" shape="square" size="sm" variant="soft">
              <IconSettings className={SIZE_MAP.sm.icon} />
            </TooltipButton>
            <TooltipButton label="Settings (medium)" shape="square" size="md" variant="soft">
              <IconSettings className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="Settings (large)" shape="square" size="lg" variant="soft">
              <IconSettings className={SIZE_MAP.lg.icon} />
            </TooltipButton>
            <span className="mx-1 h-8 w-px bg-slate-200 dark:bg-slate-700" />
            <TooltipButton label="Confirm (small)" shape="circle" size="sm" tone="emerald" variant="soft">
              <IconCheck className={SIZE_MAP.sm.icon} />
            </TooltipButton>
            <TooltipButton label="Confirm (medium)" shape="circle" size="md" tone="emerald" variant="soft">
              <IconCheck className={SIZE_MAP.md.icon} />
            </TooltipButton>
            <TooltipButton label="Confirm (large)" shape="circle" size="lg" tone="emerald" variant="soft">
              <IconCheck className={SIZE_MAP.lg.icon} />
            </TooltipButton>
          </Row>

          <Row label="Stateful toggles">
            <button
              type="button"
              aria-label={liked ? "Unlike" : "Like"}
              aria-pressed={liked}
              onClick={() => setLiked((v) => !v)}
              className={`${baseClasses("circle", "md")} ${TONE_RING.rose} hover:-translate-y-0.5 ${
                liked
                  ? "bg-rose-50 text-rose-600 hover:bg-rose-100 dark:bg-rose-500/15 dark:text-rose-400 dark:hover:bg-rose-500/25"
                  : "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200"
              }`}
            >
              {liked ? (
                <IconHeart className={`${SIZE_MAP.md.icon} ${reduceMotion ? "" : "btnio-pop"}`} />
              ) : (
                <IconHeartOutline className={SIZE_MAP.md.icon} />
              )}
              <span
                role="tooltip"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                {liked ? "Liked" : "Like"}
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </button>

            <button
              type="button"
              aria-label={saved ? "Remove bookmark" : "Bookmark"}
              aria-pressed={saved}
              onClick={() => setSaved((v) => !v)}
              className={`${baseClasses("square", "md")} ${TONE_RING.indigo} hover:-translate-y-0.5 ${
                saved
                  ? "bg-indigo-50 text-indigo-600 hover:bg-indigo-100 dark:bg-indigo-500/15 dark:text-indigo-400 dark:hover:bg-indigo-500/25"
                  : "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200"
              }`}
            >
              <IconBookmark className={`${SIZE_MAP.md.icon} ${saved && !reduceMotion ? "btnio-pop" : ""}`} filled={saved} />
              <span
                role="tooltip"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                {saved ? "Saved" : "Save"}
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </button>

            <button
              type="button"
              aria-label={muted ? "Unmute" : "Mute"}
              aria-pressed={muted}
              onClick={() => setMuted((v) => !v)}
              className={`${baseClasses("circle", "md")} ${TONE_RING.neutral} hover:-translate-y-0.5 ${
                muted
                  ? "bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white"
                  : "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
              }`}
            >
              {muted ? <IconMute className={SIZE_MAP.md.icon} /> : <IconVolume className={SIZE_MAP.md.icon} />}
              <span
                role="tooltip"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                {muted ? "Muted" : "Mute"}
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </button>

            <button
              type="button"
              aria-label={dark ? "Switch to light theme" : "Switch to dark theme"}
              aria-pressed={dark}
              onClick={() => setDark((v) => !v)}
              className={`${baseClasses("square", "md")} ${TONE_RING.indigo} hover:-translate-y-0.5 bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700`}
            >
              {dark ? <IconMoon className={SIZE_MAP.md.icon} /> : <IconSun className={SIZE_MAP.md.icon} />}
              <span
                role="tooltip"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                {dark ? "Dark" : "Light"}
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </button>
          </Row>

          <Row label="Feedback · copy & loading">
            <button
              type="button"
              aria-label={copied ? "Copied to clipboard" : "Copy install command"}
              onClick={handleCopy}
              className={`${baseClasses("square", "md")} ${TONE_RING.emerald} hover:-translate-y-0.5 ${
                copied
                  ? "bg-emerald-50 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400"
                  : "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
              }`}
            >
              {copied ? (
                <IconCheck className={`${SIZE_MAP.md.icon} ${reduceMotion ? "" : "btnio-pop"}`} />
              ) : (
                <IconCopy className={SIZE_MAP.md.icon} />
              )}
              <span
                role="tooltip"
                aria-live="polite"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                {copied ? "Copied!" : "Copy"}
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </button>

            <TooltipButton
              label={loading ? "Loading" : "Sync workspace"}
              shape="circle"
              size="md"
              tone="indigo"
              variant="soft"
              busy={loading}
              onClick={handleLoad}
            >
              {loading ? (
                <IconSpinner className={`${SIZE_MAP.md.icon} ${spin}`} />
              ) : (
                <IconCheck className={SIZE_MAP.md.icon} />
              )}
            </TooltipButton>

            <TooltipButton label="Delete item" shape="square" size="md" tone="rose" variant="ghost" onClick={() => undefined}>
              <IconTrash className={`${SIZE_MAP.md.icon} text-rose-500 group-hover:text-rose-600 dark:group-hover:text-rose-400`} />
            </TooltipButton>

            <TooltipButton label="Unavailable" shape="square" size="md" variant="outline" disabled>
              <IconSettings className={SIZE_MAP.md.icon} />
            </TooltipButton>
          </Row>

          <Row label="Social · brand glyphs">
            <a
              href="https://github.com"
              target="_blank"
              rel="noopener"
              aria-label="View on GitHub"
              className={`${baseClasses("circle", "md")} ${TONE_RING.neutral} hover:-translate-y-0.5 bg-slate-100 text-slate-700 hover:bg-slate-900 hover:text-white dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-white dark:hover:text-slate-900`}
            >
              <IconGithub className={SIZE_MAP.md.icon} />
              <span
                role="tooltip"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                GitHub
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </a>

            <a
              href="https://x.com"
              target="_blank"
              rel="noopener"
              aria-label="Follow on X"
              className={`${baseClasses("circle", "md")} ${TONE_RING.neutral} hover:-translate-y-0.5 bg-slate-100 text-slate-700 hover:bg-slate-900 hover:text-white dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-white dark:hover:text-slate-900`}
            >
              <IconX className={`${SIZE_MAP.md.icon} scale-90`} />
              <span
                role="tooltip"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                X / Twitter
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </a>

            <a
              href="https://linkedin.com"
              target="_blank"
              rel="noopener"
              aria-label="Connect on LinkedIn"
              className={`${baseClasses("circle", "md")} focus-visible:ring-sky-500 hover:-translate-y-0.5 bg-slate-100 text-[#0a66c2] hover:bg-[#0a66c2] hover:text-white dark:bg-slate-800 dark:text-sky-400 dark:hover:bg-[#0a66c2] dark:hover:text-white`}
            >
              <IconLinkedin className={SIZE_MAP.md.icon} />
              <span
                role="tooltip"
                className="btnio-tip pointer-events-none absolute bottom-[calc(100%+0.6rem)] left-1/2 z-20 whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1 text-xs font-medium text-white opacity-0 shadow-lg dark:bg-slate-100 dark:text-slate-900"
              >
                LinkedIn
                <span className="absolute left-1/2 top-full h-2 w-2 -translate-x-1/2 -translate-y-1 rotate-45 bg-slate-900 dark:bg-slate-100" />
              </span>
            </a>
          </Row>
        </div>

        <p className="mt-8 text-center text-xs text-slate-400 dark:text-slate-600">
          Hover or focus any control to reveal its tooltip · every button carries an aria-label
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →