Web InnoventixFreeCode

Glass Tooltip

Original · free

glassmorphic tooltips

byWeb InnoventixReact + Tailwind
tipglasstooltips
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/tip-glass.json
tip-glass.tsx
"use client";

import {
  cloneElement,
  useId,
  useState,
  type ReactElement,
  type ReactNode,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Placement = "top" | "bottom" | "left" | "right";

const PLACEMENT_BOX: Record<Placement, string> = {
  top: "bottom-full left-1/2 -translate-x-1/2 mb-2.5",
  bottom: "top-full left-1/2 -translate-x-1/2 mt-2.5",
  left: "right-full top-1/2 -translate-y-1/2 mr-2.5",
  right: "left-full top-1/2 -translate-y-1/2 ml-2.5",
};

const ARROW_BOX: Record<Placement, string> = {
  top: "bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 border-b border-r",
  bottom: "top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 border-t border-l",
  left: "right-0 top-1/2 translate-x-1/2 -translate-y-1/2 border-t border-r",
  right: "left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 border-b border-l",
};

const OFFSET: Record<Placement, { x: number; y: number }> = {
  top: { x: 0, y: 6 },
  bottom: { x: 0, y: -6 },
  left: { x: 6, y: 0 },
  right: { x: -6, y: 0 },
};

interface TooltipProps {
  content: ReactNode;
  children: ReactElement;
  placement?: Placement;
  forceOpen?: boolean;
}

function Tooltip({
  content,
  children,
  placement = "top",
  forceOpen = false,
}: TooltipProps) {
  const id = useId();
  const reduce = useReducedMotion();
  const [hovered, setHovered] = useState(false);
  const [focused, setFocused] = useState(false);
  const open = forceOpen || hovered || focused;

  const trigger = cloneElement(
    children as ReactElement<{ "aria-describedby"?: string }>,
    { "aria-describedby": open ? id : undefined },
  );

  const off = OFFSET[placement];

  return (
    <span
      className="relative inline-flex"
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      onFocus={() => setFocused(true)}
      onBlur={() => setFocused(false)}
      onKeyDown={(e) => {
        if (e.key === "Escape") {
          setHovered(false);
          setFocused(false);
        }
      }}
    >
      {trigger}
      <AnimatePresence>
        {open && (
          <div
            className={`pointer-events-none absolute z-50 ${PLACEMENT_BOX[placement]}`}
          >
            <motion.div
              id={id}
              role="tooltip"
              initial={
                reduce
                  ? { opacity: 0 }
                  : { opacity: 0, scale: 0.94, x: off.x, y: off.y }
              }
              animate={
                reduce
                  ? { opacity: 1 }
                  : { opacity: 1, scale: 1, x: 0, y: 0 }
              }
              exit={
                reduce
                  ? { opacity: 0 }
                  : { opacity: 0, scale: 0.94, x: off.x, y: off.y }
              }
              transition={{
                duration: reduce ? 0.14 : 0.22,
                ease: [0.16, 1, 0.3, 1],
              }}
              className="relative w-max max-w-[16rem] rounded-2xl border border-white/50 bg-white/55 px-3.5 py-2.5 text-sm leading-snug text-slate-800 shadow-xl shadow-slate-900/10 ring-1 ring-white/40 backdrop-blur-xl backdrop-saturate-150 dark:border-white/10 dark:bg-slate-900/55 dark:text-slate-100 dark:shadow-black/40 dark:ring-white/5"
            >
              <span
                aria-hidden="true"
                className="pointer-events-none absolute inset-x-0 top-0 h-1/2 rounded-t-2xl bg-gradient-to-b from-white/55 to-transparent dark:from-white/10"
              />
              <div className="relative">{content}</div>
              <span
                aria-hidden="true"
                className={`absolute z-[-1] size-2.5 rotate-45 rounded-[3px] border-white/50 bg-white/55 backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/55 ${ARROW_BOX[placement]}`}
              />
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </span>
  );
}

const PLACEMENTS: { value: Placement; label: string }[] = [
  { value: "top", label: "Top" },
  { value: "bottom", label: "Bottom" },
  { value: "left", label: "Left" },
  { value: "right", label: "Right" },
];

function StarIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-5"
      aria-hidden="true"
    >
      <path d="M12 3.5l2.6 5.3 5.9.86-4.25 4.14 1 5.86L12 17.9l-5.25 2.76 1-5.86L3.5 9.66l5.9-.86L12 3.5z" />
    </svg>
  );
}

function BellIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-5"
      aria-hidden="true"
    >
      <path d="M6 9a6 6 0 0112 0c0 4.5 1.5 6 1.5 6h-15S6 13.5 6 9z" />
      <path d="M10 20a2 2 0 004 0" />
    </svg>
  );
}

function LinkIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-5"
      aria-hidden="true"
    >
      <path d="M10 13.5a3.5 3.5 0 005 0l3-3a3.5 3.5 0 00-5-5l-1.5 1.5" />
      <path d="M14 10.5a3.5 3.5 0 00-5 0l-3 3a3.5 3.5 0 005 5l1.5-1.5" />
    </svg>
  );
}

function HelpIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.7}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-4"
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M9.4 9.2a2.6 2.6 0 015 .8c0 1.7-2.4 2-2.4 3.5" />
      <path d="M12 17h.01" />
    </svg>
  );
}

function RocketIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-4"
      aria-hidden="true"
    >
      <path d="M5 15c-1.5 1-2 4-2 4s3-.5 4-2" />
      <path d="M14.5 4.5c3.5 1 4.5 6 4.5 6l-6 6-4-4 6-6c-.5-1-1-2 0-2z" />
      <circle cx="14" cy="9" r="1.2" />
    </svg>
  );
}

export default function TipGlass() {
  const [placement, setPlacement] = useState<Placement>("top");
  const [pinned, setPinned] = useState(false);

  const iconBtn =
    "inline-flex size-11 items-center justify-center rounded-xl border border-slate-200/80 bg-white/70 text-slate-600 backdrop-blur transition-colors hover:border-indigo-300 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700/80 dark:bg-slate-800/60 dark:text-slate-300 dark:hover:border-indigo-500/60 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-950";

  return (
    <section className="relative w-full bg-slate-50 px-6 py-20 sm:py-28 dark:bg-slate-950">
      <style>{`
        @keyframes tipglass-float {
          0%, 100% { transform: translate3d(0, 0, 0); }
          50% { transform: translate3d(0, -22px, 0); }
        }
        @keyframes tipglass-sheen {
          0% { transform: translateX(-130%); }
          100% { transform: translateX(230%); }
        }
        @media (prefers-reduced-motion: reduce) {
          .tipglass-anim { animation: none !important; }
        }
      `}</style>

      {/* Atmospheric backdrop so the frosted glass has something to refract */}
      <div className="pointer-events-none absolute inset-0 overflow-hidden">
        <div
          className="tipglass-anim absolute -left-16 top-4 size-72 rounded-full bg-indigo-400/30 blur-3xl dark:bg-indigo-500/20"
          style={{ animation: "tipglass-float 13s ease-in-out infinite" }}
        />
        <div
          className="tipglass-anim absolute right-[-4rem] top-40 size-80 rounded-full bg-violet-400/25 blur-3xl dark:bg-violet-500/20"
          style={{ animation: "tipglass-float 17s ease-in-out infinite 1.5s" }}
        />
        <div
          className="tipglass-anim absolute bottom-[-3rem] left-1/3 size-72 rounded-full bg-emerald-300/25 blur-3xl dark:bg-emerald-500/15"
          style={{ animation: "tipglass-float 15s ease-in-out infinite 0.8s" }}
        />
        <div
          className="absolute inset-0 opacity-[0.6] dark:opacity-40"
          style={{
            backgroundImage:
              "linear-gradient(to right, rgba(100,116,139,0.14) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,0.14) 1px, transparent 1px)",
            backgroundSize: "56px 56px",
            maskImage:
              "radial-gradient(ellipse 80% 60% at 50% 40%, black, transparent 75%)",
            WebkitMaskImage:
              "radial-gradient(ellipse 80% 60% at 50% 40%, black, transparent 75%)",
          }}
        />
      </div>

      <div className="relative mx-auto max-w-5xl">
        <header className="max-w-2xl">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200/80 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-[0.18em] text-slate-500 backdrop-blur dark:border-slate-700/80 dark:bg-slate-900/60 dark:text-slate-400">
            Overlay / Tooltip
          </span>
          <h2 className="mt-5 flex flex-wrap items-center gap-x-3 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
            Glass tooltips that read in any light
            <Tooltip
              placement="bottom"
              content="Every trigger below responds to mouse hover, keyboard focus, and the Escape key — try tabbing through the page."
            >
              <button
                type="button"
                aria-label="How these tooltips behave"
                className="inline-flex size-6 items-center justify-center rounded-full border border-slate-300 text-slate-500 transition-colors hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-600 dark:text-slate-400 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-950"
              >
                <HelpIcon />
              </button>
            </Tooltip>
          </h2>
          <p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-slate-300">
            Frosted, backdrop-blurred hints that stay legible over photos,
            gradients, and dark surfaces. No layout shift, no clipped edges — the
            blur does the contrast work so the copy never fights the background.
          </p>
        </header>

        <div className="mt-12 grid gap-6 lg:grid-cols-5">
          {/* Playground */}
          <div className="lg:col-span-3">
            <div className="relative h-full overflow-hidden rounded-3xl border border-slate-200/80 bg-white/70 p-7 backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/50">
              <span
                aria-hidden="true"
                className="pointer-events-none absolute inset-x-0 top-0 h-px overflow-hidden"
              >
                <span
                  className="tipglass-anim absolute inset-y-0 w-1/3 bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent dark:via-indigo-300/60"
                  style={{ animation: "tipglass-sheen 6s ease-in-out infinite" }}
                />
              </span>

              <h3 className="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                Playground
              </h3>
              <p className="mt-1 text-sm text-slate-600 dark:text-slate-300">
                Pick a side, then hover or focus the button. Pin it open to
                inspect the frosted layers up close.
              </p>

              <div
                role="radiogroup"
                aria-label="Tooltip placement"
                className="mt-6 inline-flex flex-wrap gap-1.5 rounded-2xl border border-slate-200/80 bg-slate-100/70 p-1.5 dark:border-slate-700/80 dark:bg-slate-800/50"
              >
                {PLACEMENTS.map((p) => (
                  <label key={p.value} className="cursor-pointer">
                    <input
                      type="radio"
                      name="tipglass-placement"
                      value={p.value}
                      checked={placement === p.value}
                      onChange={() => setPlacement(p.value)}
                      className="peer sr-only"
                    />
                    <span className="inline-flex select-none rounded-xl px-4 py-1.5 text-sm font-medium text-slate-600 transition-colors peer-checked:bg-white peer-checked:text-indigo-600 peer-checked:shadow-sm peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-slate-100 dark:text-slate-300 dark:peer-checked:bg-slate-900 dark:peer-checked:text-indigo-300 dark:peer-focus-visible:ring-offset-slate-800">
                      {p.label}
                    </span>
                  </label>
                ))}
              </div>

              <div className="mt-4">
                <label className="inline-flex cursor-pointer items-center gap-3">
                  <span className="relative inline-block h-6 w-11 shrink-0">
                    <input
                      type="checkbox"
                      checked={pinned}
                      onChange={(e) => setPinned(e.target.checked)}
                      className="peer sr-only"
                    />
                    <span className="absolute inset-0 rounded-full bg-slate-300 transition-colors peer-checked:bg-indigo-500 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:bg-slate-700 dark:peer-focus-visible:ring-offset-slate-900" />
                    <span className="absolute left-0.5 top-0.5 size-5 rounded-full bg-white shadow transition-transform peer-checked:translate-x-5" />
                  </span>
                  <span className="text-sm font-medium text-slate-700 dark:text-slate-200">
                    Keep tooltip pinned
                  </span>
                </label>
              </div>

              <div className="mt-10 flex min-h-[8rem] items-center justify-center rounded-2xl border border-dashed border-slate-300/70 py-8 dark:border-slate-700/70">
                <Tooltip
                  placement={placement}
                  forceOpen={pinned}
                  content={
                    <div className="space-y-2">
                      <p className="flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
                        <span className="text-emerald-500 dark:text-emerald-400">
                          <RocketIcon />
                        </span>
                        Ships in about 40 seconds
                      </p>
                      <p className="text-xs text-slate-600 dark:text-slate-300">
                        Runs the full test suite, then rolls out to three edge
                        regions behind an automatic canary.
                      </p>
                      <p className="flex items-center gap-1.5 pt-0.5 text-xs text-slate-500 dark:text-slate-400">
                        <kbd className="rounded-md border border-slate-300/70 bg-white/60 px-1.5 py-0.5 font-sans text-[11px] font-semibold text-slate-700 dark:border-slate-600/70 dark:bg-slate-800/70 dark:text-slate-200">
                          ⌘
                        </kbd>
                        <kbd className="rounded-md border border-slate-300/70 bg-white/60 px-1.5 py-0.5 font-sans text-[11px] font-semibold text-slate-700 dark:border-slate-600/70 dark:bg-slate-800/70 dark:text-slate-200">
                          ↵
                        </kbd>
                        to confirm
                      </p>
                    </div>
                  }
                >
                  <button
                    type="button"
                    className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
                  >
                    <RocketIcon />
                    Deploy to production
                  </button>
                </Tooltip>
              </div>
            </div>
          </div>

          {/* In context */}
          <div className="lg:col-span-2">
            <div className="flex h-full flex-col gap-6 rounded-3xl border border-slate-200/80 bg-white/70 p-7 backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/50">
              <div>
                <h3 className="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                  In context
                </h3>
                <p className="mt-3 text-[15px] leading-relaxed text-slate-700 dark:text-slate-200">
                  Your changes go live behind a{" "}
                  <Tooltip
                    placement="top"
                    content="A switch that ships code dark, then reveals it to a slice of traffic — roll back instantly with no redeploy."
                  >
                    <button
                      type="button"
                      className="rounded font-medium text-indigo-700 underline decoration-dotted decoration-indigo-400 underline-offset-4 transition-colors hover:text-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-300 dark:focus-visible:ring-offset-slate-900"
                    >
                      feature flag
                    </button>
                  </Tooltip>{" "}
                  rollout, so a bad deploy is one toggle away from reverted.
                </p>
              </div>

              <div className="mt-auto border-t border-slate-200/70 pt-6 dark:border-slate-700/70">
                <p className="mb-3 text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
                  Icon toolbar
                </p>
                <div className="flex items-center gap-3">
                  <Tooltip placement="bottom" content="Add to favorites">
                    <button type="button" aria-label="Add to favorites" className={iconBtn}>
                      <StarIcon />
                    </button>
                  </Tooltip>
                  <Tooltip placement="bottom" content="Mute notifications">
                    <button type="button" aria-label="Mute notifications" className={iconBtn}>
                      <BellIcon />
                    </button>
                  </Tooltip>
                  <Tooltip placement="bottom" content="Copy share link">
                    <button type="button" aria-label="Copy share link" className={iconBtn}>
                      <LinkIcon />
                    </button>
                  </Tooltip>
                </div>
                <p className="mt-5 text-xs text-slate-500 dark:text-slate-400">
                  Icon-only controls carry an <code className="rounded bg-slate-100 px-1 py-0.5 text-[11px] text-slate-700 dark:bg-slate-800 dark:text-slate-300">aria-label</code>{" "}
                  for the name and the tooltip as an{" "}
                  <code className="rounded bg-slate-100 px-1 py-0.5 text-[11px] text-slate-700 dark:bg-slate-800 dark:text-slate-300">aria-describedby</code>{" "}
                  hint.
                </p>
              </div>
            </div>
          </div>
        </div>
      </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 →