A frosted-glass tooltip that follows the cursor with a 22 px offset, appearing when you hover each phrase of a four-part sentence. The pill uses backdrop-filter blur at 40 px plus saturate 1.9 and a translucent white gradient fill with an inset highlight border, giving it the liquid-glass look of visionOS. Non-hovered phrases dim to 0.35 opacity and pick up a 1.5 px blur, creating a shallow depth-of-field focus effect around the active word. Entrance and exit are staggered -- opacity at 460 ms, blur at 540 ms, scale at 500 ms -- so the pill materialises rather than snaps.
GIRAGIRAis a labfor things thatcatch light.
Hover each phrase to see the liquid-glass tooltip
Source
"use client";
import { useEffect, useRef, useState } from "react";
import ScrambleLink from "../ScrambleLink";
interface HintConfig {
text: string;
hint: string;
}
const HINTS: HintConfig[] = [
{ text: "GIRAGIRA", hint: "Glare — from Japanese ギラギラ" },
{ text: "is a lab", hint: "Experiments in type, colour & motion" },
{ text: "for things that", hint: "No rules, just play" },
{ text: "catch light.", hint: "✦" },
];
const OFFSET = 22;
export default function GlassText() {
const [activeHint, setActiveHint] = useState<string | null>(null);
const [activeIdx, setActiveIdx] = useState<number | null>(null);
const hintRef = useRef<HTMLDivElement>(null);
const cursorRef = useRef({ x: 0, y: 0 });
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const onMove = (e: MouseEvent) => {
cursorRef.current = { x: e.clientX, y: e.clientY };
const el = hintRef.current;
if (!el || !activeHint) return;
const w = el.offsetWidth || 200;
const h = el.offsetHeight || 48;
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = e.clientX + OFFSET;
let top = e.clientY + OFFSET;
if (left + w > vw - 12) left = e.clientX - w - OFFSET;
if (top + h > vh - 12) top = e.clientY - h - OFFSET;
if (left < 12) left = 12;
if (top < 12) top = 12;
el.style.left = `${left}px`;
el.style.top = `${top}px`;
};
document.addEventListener("mousemove", onMove);
return () => document.removeEventListener("mousemove", onMove);
}, [activeHint]);
const show = (hint: string, idx: number) => {
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
hideTimerRef.current = null;
}
setActiveHint(hint);
setActiveIdx(idx);
requestAnimationFrame(() => {
const el = hintRef.current;
if (!el) return;
const c = cursorRef.current;
const w = el.offsetWidth || 200;
const h = el.offsetHeight || 48;
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = c.x + OFFSET;
let top = c.y + OFFSET;
if (left + w > vw - 12) left = c.x - w - OFFSET;
if (top + h > vh - 12) top = c.y - h - OFFSET;
el.style.left = `${left}px`;
el.style.top = `${top}px`;
});
};
const hide = () => {
hideTimerRef.current = setTimeout(() => {
setActiveHint(null);
setActiveIdx(null);
hideTimerRef.current = null;
}, 120);
};
return (
<div className="fixed inset-0 flex items-center justify-center select-none overflow-hidden">
{/* Gradient background for the glass effect to read against */}
<div
className="absolute inset-0"
style={{
background: "linear-gradient(135deg, #dcd9fb 0%, #f99a9e 25%, #fbd9da 45%, #ecdca7 65%, #afcae4 85%, #daebfb 100%)",
}}
/>
{/* Soft noise texture overlay */}
<div
className="absolute inset-0 opacity-20"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.4'/%3E%3C/svg%3E")`,
backgroundSize: "200px 200px",
}}
/>
{/* Glass hint pill */}
<div
ref={hintRef}
className="fixed pointer-events-none"
style={{
zIndex: 200,
background:
"linear-gradient(135deg, rgba(255,255,255,0.30) 0%, rgba(255,255,255,0.08) 100%)",
color: "#0a0a0a",
borderRadius: "999px",
padding: "14px 24px",
fontFamily: "system-ui, -apple-system, sans-serif",
fontSize: "18px",
fontWeight: 500,
letterSpacing: "-0.005em",
whiteSpace: "nowrap",
backdropFilter: "blur(40px) saturate(1.9) brightness(1.04)",
WebkitBackdropFilter: "blur(40px) saturate(1.9) brightness(1.04)",
border: "1px solid rgba(255,255,255,0.45)",
boxShadow: `
inset 0 1px 0 rgba(255,255,255,0.7),
inset 0 -1px 0 rgba(255,255,255,0.08),
0 1px 2px rgba(15,17,22,0.04),
0 14px 36px rgba(15,17,22,0.10),
0 32px 72px rgba(15,17,22,0.10)
`,
opacity: activeHint ? 1 : 0,
filter: activeHint ? "blur(0)" : "blur(10px)",
transform: activeHint ? "scale(1)" : "scale(0.96)",
transformOrigin: "top left",
transition: `
opacity 460ms cubic-bezier(0.22, 1, 0.36, 1),
filter 540ms cubic-bezier(0.22, 1, 0.36, 1),
transform 500ms cubic-bezier(0.22, 1, 0.36, 1)
`,
}}
>
{activeHint}
</div>
{/* Text content */}
<div className="relative z-10 text-center px-8">
<p
className="flex flex-wrap justify-center gap-x-[0.35em] leading-[1.2]"
style={{
fontFamily: "var(--font-flux), Georgia, serif",
fontSize: "clamp(28px, 5vw, 64px)",
fontVariationSettings: "'wght' 300, 'SRIF' 0",
color: "rgba(255,255,255,0.95)",
}}
>
{HINTS.map((item, i) => {
const isActive = activeIdx === i;
const hasSomeActive = activeIdx !== null;
return (
<span
key={i}
className="inline-block cursor-pointer italic"
style={{
textDecorationLine: "underline",
textDecorationThickness: "1px",
textUnderlineOffset: "0.16em",
textDecorationColor: isActive
? "rgba(255,255,255,0.7)"
: "rgba(255,255,255,0.4)",
opacity: hasSomeActive ? (isActive ? 1 : 0.35) : 1,
transform: isActive ? "translateY(-3px) scale(1.03)" : "none",
filter: hasSomeActive && !isActive ? "blur(1.5px)" : "none",
transition:
"opacity 340ms cubic-bezier(0.22, 1, 0.36, 1), transform 400ms cubic-bezier(0.22, 1, 0.36, 1), filter 340ms cubic-bezier(0.22, 1, 0.36, 1), text-decoration-color 280ms ease",
}}
onMouseEnter={() => show(item.hint, i)}
onMouseLeave={hide}
>
{item.text}
</span>
);
})}
</p>
<p
className="mt-10 text-white/40"
style={{
fontSize: "13px",
fontVariationSettings: "'wght' 300, 'SRIF' 100",
letterSpacing: "0.04em",
}}
>
Hover each phrase to see the liquid-glass tooltip
</p>
</div>
<div className="fixed bottom-6 left-6 z-10">
<ScrambleLink
from="GLASS"
to="← LAB"
href="/lab"
className="text-[11px] tracking-[0.1em] text-white/30 uppercase hover:text-white/60 transition-colors"
style={{ fontVariationSettings: "'wght' 400, 'SRIF' 100" }}
/>
</div>
</div>
);
}