Xray Lens
A loupe over a polished UI. Inside the glass the surface peels away to the layer underneath, such as model tokens, raw JSON or latency telemetry, lined up with the parts it explains. A switch floods it over the whole card.
- Hover
- Interaction
- Developer
Uses motion, clsx, tailwind-merge
I was charged twice for Pro this month, once on Oct 2 and again on Oct 3. Could you refund the extra $49.00?
Hi Maya, sorry about that. I've refunded the duplicate $49.00 charge, so it should be back on your card in 5 to 7 days.
Point at the ticket to see what the model read and returned, or flip the whole card.
import type { ReactNode } from "react";
import { XrayLens, XrayLensSpot, XrayLensText } from "@/components/ui/xray-lens";
// A spot's box in the xray: a dashed outline exactly where the UI element sits.
const box = "outline-1 -outline-offset-1 outline-dashed outline-sky-400/50 bg-sky-400/[0.07]";
function Value({ children, className }: { children: ReactNode; className?: string }) {
return (
<span
className={`grid size-full place-items-center font-mono text-[11px] leading-none whitespace-nowrap ${className ?? "text-amber-300"}`}
>
{children}
</span>
);
}
function Chip({ dot, children }: { dot: string; children: ReactNode }) {
return (
<span className="inline-flex h-6 items-center gap-1.5 rounded-full bg-zinc-100 px-2.5 text-xs font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
<span className={`size-1.5 rounded-full ${dot}`} />
{children}
</span>
);
}
const MESSAGE =
"I was charged twice for Pro this month, once on Oct 2 and again on Oct 3. Could you refund the extra $49.00?";
// The reply as the model streamed it: each token with its probability. The dates and the
// amount are where it was least sure.
const REPLY: [string, number][] = [
["Hi", 0.99], [" Maya", 0.98], [",", 0.99], [" sorry", 0.93], [" about", 0.97], [" that", 0.98],
[".", 0.99], [" I", 0.95], ["'ve", 0.97], [" refunded", 0.9], [" the", 0.99], [" duplicate", 0.88],
[" $", 0.7], ["49", 0.62], [".", 0.9], ["00", 0.95], [" charge", 0.97], [",", 0.96], [" so", 0.9],
[" it", 0.97], [" should", 0.82], [" be", 0.99], [" back", 0.96], [" on", 0.95], [" your", 0.98],
[" card", 0.97], [" in", 0.94], [" 5", 0.41], [" to", 0.7], [" 7", 0.38], [" days", 0.93], [".", 0.99],
];
function Ticket() {
return (
<div className="flex flex-col gap-5 p-5 sm:p-6">
<div className="flex items-start justify-between gap-4">
<XrayLensSpot
label="customer = cus_8Hq2nL"
xrayClassName={box}
xray={
<span className="flex h-full flex-col justify-center gap-1.5 pl-2 font-mono text-[11px] leading-none">
<span className="text-emerald-300">"cus_8Hq2nL"</span>
<span className="text-zinc-400">
plan <span className="text-emerald-300">pro</span> · ltv{" "}
<span className="text-amber-300">1188</span>
</span>
</span>
}
className="flex items-center gap-3"
>
<span className="grid size-10 place-items-center rounded-full bg-gradient-to-br from-amber-200 to-rose-300 text-sm font-semibold text-rose-950">
MC
</span>
<span className="flex flex-col">
<span className="text-sm font-medium text-zinc-900 dark:text-zinc-50">Maya Chen</span>
<span className="text-xs text-zinc-500">maya@northwind.io</span>
</span>
</XrayLensSpot>
<XrayLensSpot
label="received_at"
xrayClassName={box}
xray={<Value>1760531412</Value>}
className="pt-1 text-xs text-zinc-500 tabular-nums"
>
2 min ago
</XrayLensSpot>
</div>
<div className="flex flex-wrap gap-2">
<XrayLensSpot
label="classify.intent"
xrayClassName={box}
xray={<Value className="text-emerald-300">"refund"</Value>}
>
<Chip dot="bg-sky-500">Billing</Chip>
</XrayLensSpot>
<XrayLensSpot label="classify.sentiment" xrayClassName={box} xray={<Value>-0.72</Value>}>
<Chip dot="bg-rose-500">Frustrated</Chip>
</XrayLensSpot>
<XrayLensSpot label="route.priority = p1" xrayClassName={box} xray={<Value>sla 2h</Value>}>
<Chip dot="bg-amber-500">Urgent</Chip>
</XrayLensSpot>
</div>
<XrayLensText
text={MESSAGE}
entities={[
{ text: "charged twice", label: "extract.issue = duplicate_charge" },
{ text: "$49.00", label: "extract.amount = 4900" },
]}
label="input · 31 tokens"
className="text-[15px] leading-relaxed text-pretty text-zinc-700 dark:text-zinc-300"
/>
<XrayLensSpot
label="generate.reply"
xrayClassName={box}
xray={
<span className="absolute top-4 left-4 font-mono text-[11px] leading-4">
<span className="text-violet-300">generate</span>
<span className="text-zinc-400">
() · <span className="text-amber-300">61</span> tok · <span className="text-amber-300">412</span>ms
</span>
</span>
}
className="rounded-xl bg-zinc-50 p-4 ring-1 ring-zinc-950/5 dark:bg-zinc-900 dark:ring-white/10"
>
<div className="flex items-center justify-between text-xs leading-4">
<span className="font-medium text-zinc-900 dark:text-zinc-100">Suggested reply</span>
<XrayLensSpot
as="span"
label="eval.confidence"
xrayClassName={box}
xray={<Value>p 0.94</Value>}
className="inline-block text-zinc-500 tabular-nums"
>
94% confident
</XrayLensSpot>
</div>
<XrayLensText
tokens={REPLY.map(([text, p]) => ({ text, p }))}
label="output · logprobs"
className="mt-2 text-sm leading-relaxed text-pretty text-zinc-600 dark:text-zinc-400"
/>
</XrayLensSpot>
<div className="flex items-center gap-2">
<XrayLensSpot
label="tool_call · stripe"
xrayClassName={box}
xray={<Value className="text-violet-300">refunds.create()</Value>}
>
<button
type="button"
className="h-9 rounded-lg bg-zinc-900 px-3.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
Refund $49.00
</button>
</XrayLensSpot>
<XrayLensSpot
label="tool_call · helpdesk"
xrayClassName={box}
xray={<Value className="text-violet-300">reply.send()</Value>}
>
<button
type="button"
className="h-9 rounded-lg px-3.5 text-sm font-medium text-zinc-700 ring-1 ring-zinc-950/10 transition-colors hover:bg-zinc-50 dark:text-zinc-300 dark:ring-white/15 dark:hover:bg-zinc-900"
>
Send reply
</button>
</XrayLensSpot>
</div>
</div>
);
}
export function XrayLensDemo() {
return (
<section className="w-full max-w-xl">
<XrayLens
size="36%"
rest={{ x: 0.26, y: 0.26 }}
labels={["Ticket", "Model"]}
toggleClassName="top-full right-auto bottom-auto left-1/2 mt-4 -translate-x-1/2"
surface={<Ticket />}
xray={
<div className="absolute inset-0 bg-[linear-gradient(rgb(56_189_248/0.07)_1px,transparent_1px),linear-gradient(90deg,rgb(56_189_248/0.07)_1px,transparent_1px)] bg-size-[16px_16px]" />
}
xrayClassName="bg-[#060a12] text-sky-100"
className="rounded-2xl bg-white shadow-[0_1px_2px_rgb(0_0_0/0.05),0_16px_40px_-20px_rgb(0_0_0/0.3)] ring-1 ring-zinc-950/5 dark:bg-zinc-950 dark:ring-white/10"
/>
<p className="mt-20 text-center text-sm text-zinc-500">
Point at the ticket to see what the model read and returned, or flip the whole card.
</p>
</section>
);
}
Installation
Add the component on its own with the CLI, or copy the file in by hand.
The CLI writes components/ui/xray-lens.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.
npx shadcn@latest add @keyframeui/xray-lensInstall the dependencies
npm i motion clsx tailwind-mergeAdd the cn helper
Skip this if you already use shadcn/ui.
lib/utils.tsimport { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }Copy the source into your project
components/ui/xray-lens.tsx"use client"; import * as React from "react"; import { animate, cancelFrame, frame, useMotionValue, useSpring, type AnimationPlaybackControls, type MotionValue, } from "motion/react"; import { cn } from "@/lib/utils"; /* ───────────────────────────────────────────────────────── * XRAY LENS STORYBOARD * * rest the surface fills the frame. The xray waits under it, * in the same box, so every part lines up with the part * it explains * intro the first time the frame scrolls into view, the loupe * opens at `rest`: it grows in on a soft spring and the * magnification swells from 1 to `zoom` as it opens * hover the loupe flies to the pointer and trails it on a * stiff spring, so it moves like a piece of glass. Inside * it the surface is gone and the xray shows through, * magnified around the lens's center * glint the highlight on the glass slides against the motion * label when the lens centers on a spot, its label hangs * under the glass * leave the loupe glides back to `rest`, or closes with * `rest={false}` * tour on touch screens (or everywhere, with autoPlay) the * loupe visits each labeled spot in reading order while * the frame is in view, pausing on each. A finger, the * pointer or focus takes over; the tour picks up again * from the nearest spot once they let go * drag with mode="drag" (and on touch screens) the loupe stays * put until it's picked up. Held, it lifts: it grows a * little, magnifies a little more and its shadow drops. * A tap anywhere else glides it there * reveal the switch floods the xray across the whole frame from * the switch, or from the lens if it's open: the glass * swells and fades as the circle grows past the corners, * and the magnification settles to 1 so everything lines * up. Switching back pulls the flood in to the lens * keys the lens is focusable; arrow keys move it, Shift for * bigger steps, Home sends it back to `rest` * reduced the lens follows the pointer with no lag, opens without * growing and never tours; the switch flips at once * ───────────────────────────────────────────────────────── */ const LENS = { follow: { stiffness: 420, damping: 38, mass: 0.7 }, // the glass trailing the pointer glide: { type: "spring", visualDuration: 0.6, bounce: 0.06 }, // back to rest, to a tap, between tour stops open: { type: "spring", visualDuration: 0.45, bounce: 0.22 }, close: { type: "spring", visualDuration: 0.25, bounce: 0 }, lift: { type: "spring", visualDuration: 0.3, bounce: 0.3 }, flood: { duration: 0.85, ease: "easeInOut" }, // the reveal across the whole frame liftGrow: 0.05, // the lens grows by this much while it's held liftZoom: 0.05, // and magnifies this much more liftDrop: 10, // px its shadow drops glint: 0.004, // px of glint travel per px/s of speed glintMax: 8, step: 16, // px per arrow key stepLarge: 64, // with Shift labelGap: 10, // px between the glass and its label dwell: 2200, // ms from one tour stop to the next, glide included tourStart: 900, // ms after the frame comes into view resume: 1800, // ms after a finger lifts, a drag ends or focus leaves resumeMouse: 700, // ms after the pointer leaves, with autoPlay={true} minSize: 24, } as const; // Where the tour wanders when no spot has a label, as fractions of the frame. const WANDER: [number, number][] = [ [0.3, 0.35], [0.7, 0.3], [0.66, 0.7], [0.32, 0.66], ]; const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v)); /** The lens's diameter in px: px as given, or a percentage of the frame's width, never wider than the frame. */ function lensSize(size: number | string, w: number, h: number) { const text = String(size).trim(); const px = typeof size === "number" ? size : text.endsWith("%") ? (parseFloat(text) / 100) * w : parseFloat(text); const want = Number.isFinite(px) ? px : 200; const most = Math.min(w, h) * 0.9; return most > 0 ? clamp(want, LENS.minSize, Math.max(LENS.minSize, most)) : want; } /* ── Spots ──────────────────────────────────────────────── */ type Rect = { x: number; y: number; w: number; h: number }; type SpotEntry = { id: string; xray: React.ReactNode; className?: string; label?: string; depth: number }; type SpotTag = "div" | "span" | "p" | "li" | "blockquote"; /** * Spots live in the surface but draw in the xray layer. Each one measures its * own element and hands the box to this store; the xray layer renders a box * per spot, and the lens reads the labels from here. */ function createSpots() { let root: HTMLDivElement | null = null; let list: SpotEntry[] = []; const empty: SpotEntry[] = []; const rects = new Map<string, Rect & { radius: string }>(); const boxes = new Map<string, HTMLDivElement>(); const measurers = new Set<() => void>(); const listeners = new Set<() => void>(); const paint = (id: string) => { const box = boxes.get(id); const rect = rects.get(id); if (!box || !rect) return; box.style.transform = `translate(${rect.x}px, ${rect.y}px)`; box.style.width = `${rect.w}px`; box.style.height = `${rect.h}px`; box.style.borderRadius = rect.radius; }; return { connect(el: HTMLDivElement | null) { root = el; measurers.forEach((measure) => measure()); }, measureAll() { measurers.forEach((measure) => measure()); }, /** A box in root-local px, with any CSS scale on the way divided out. */ measure(id: string, el: HTMLElement) { if (!root) return; const frameRect = root.getBoundingClientRect(); const kx = root.offsetWidth ? frameRect.width / root.offsetWidth : 1; const ky = root.offsetHeight ? frameRect.height / root.offsetHeight : 1; const r = el.getBoundingClientRect(); // The spot's wrapper is usually square; take the corners from what it wraps. let radius = getComputedStyle(el).borderRadius; if ((!radius || radius === "0px") && el.firstElementChild) { radius = getComputedStyle(el.firstElementChild).borderRadius; } rects.set(id, { x: (r.left - frameRect.left) / kx - root.clientLeft, y: (r.top - frameRect.top) / ky - root.clientTop, w: r.width / kx, h: r.height / ky, radius, }); paint(id); }, upsert(entry: SpotEntry) { const i = list.findIndex((s) => s.id === entry.id); const prev = list[i]; if ( prev && prev.xray === entry.xray && prev.className === entry.className && prev.label === entry.label && prev.depth === entry.depth ) { return; } list = i === -1 ? [...list, entry] : list.map((s, j) => (j === i ? entry : s)); listeners.forEach((l) => l()); }, remove(id: string) { rects.delete(id); list = list.filter((s) => s.id !== id); listeners.forEach((l) => l()); }, addMeasurer(fn: () => void) { measurers.add(fn); return () => { measurers.delete(fn); }; }, attachBox(id: string, el: HTMLDivElement | null) { if (el) { boxes.set(id, el); paint(id); } else { boxes.delete(id); } }, /** The label of the smallest spot that contains a point. */ labelAt(x: number, y: number) { let best: string | undefined; let area = Infinity; for (const s of list) { const r = rects.get(s.id); if (!s.label || !r) continue; if (x < r.x || y < r.y || x > r.x + r.w || y > r.y + r.h) continue; if (r.w * r.h < area) { area = r.w * r.h; best = s.label; } } return best; }, /** The centers of the labeled spots, in reading order. */ stops() { const points: { x: number; y: number }[] = []; for (const s of list) { const r = rects.get(s.id); if (s.label && r) points.push({ x: r.x + r.w / 2, y: r.y + r.h / 2 }); } return points.sort((a, b) => Math.round(a.y / 24) - Math.round(b.y / 24) || a.x - b.x); }, subscribe(listener: () => void) { listeners.add(listener); return () => { listeners.delete(listener); }; }, snapshot: () => list, serverSnapshot: () => empty, }; } type Spots = ReturnType<typeof createSpots>; const XrayLensContext = React.createContext<Spots | null>(null); /** How many spots wrap this one, so a nested spot draws over its parent. */ const SpotDepth = React.createContext(0); /** Draws a box in the xray layer for every spot in the surface. */ function SpotLayer({ spots }: { spots: Spots }) { const list = React.useSyncExternalStore(spots.subscribe, spots.snapshot, spots.serverSnapshot); return list.map((spot) => ( <div key={spot.id} ref={(el) => { spots.attachBox(spot.id, el); return () => spots.attachBox(spot.id, null); }} className={cn("absolute top-0 left-0", spot.className)} style={{ zIndex: spot.depth }} > {spot.xray} </div> )); } export interface XrayLensSpotProps extends React.HTMLAttributes<HTMLElement> { /** What this element looks like under the lens. Drawn in the xray layer over the element's exact box. */ xray: React.ReactNode; /** Named under the glass while the lens is centered on this element, and a stop on the tour. */ label?: string; /** The wrapper element. Use `"span"` inside a paragraph. */ as?: SpotTag; /** Classes for the spot's box in the xray layer. It gets the element's size and corner radius. */ xrayClassName?: string; } /** * Marks part of the surface and gives it an xray of its own, laid over the * element's box so the two line up wherever the layout puts it. */ export function XrayLensSpot({ xray, label, as = "div", xrayClassName, children, ...props }: XrayLensSpotProps) { const spots = React.useContext(XrayLensContext); const depth = React.useContext(SpotDepth); const id = React.useId(); const ref = React.useRef<HTMLElement>(null); React.useLayoutEffect(() => { spots?.upsert({ id, xray, className: xrayClassName, label, depth }); }); React.useLayoutEffect(() => { const el = ref.current; if (!spots || !el) return; const measure = () => spots.measure(id, el); const sized = new ResizeObserver(measure); sized.observe(el); const stop = spots.addMeasurer(measure); measure(); return () => { sized.disconnect(); stop(); spots.remove(id); }; }, [spots, id]); return React.createElement( as, { ref, ...props }, <SpotDepth.Provider value={depth + 1}>{children}</SpotDepth.Provider>, ); } /* ── Text as tokens ─────────────────────────────────────── */ /** A token as your model returned it, with its probability (`p`, 0–1) or classes of its own. A `label` makes it a spot. */ export type XrayLensToken = string | { text: string; p?: number; label?: string; className?: string }; /** A phrase to mark in the text, e.g. an entity the model extracted. */ export type XrayLensEntity = { text: string; label: string }; type Token = { text: string; p?: number; label?: string; className?: string }; const ALTERNATE = ["bg-violet-400/25 text-violet-100", "bg-sky-400/20 text-sky-100"]; const ENTITY_BOX = "outline-1 -outline-offset-1 outline-dashed outline-amber-300/80 bg-amber-300/10"; // The copy sits in a box the size of the text's border box, so it drops the margin and fills the box. const COPY_STYLE: React.CSSProperties = { margin: 0, width: "100%", maxWidth: "none", background: "none" }; const confidence = (p: number) => p >= 0.85 ? "bg-emerald-400/15 text-emerald-100" : p >= 0.5 ? "bg-amber-400/30 text-amber-100" : "bg-rose-500/35 text-rose-100"; /** Splits text roughly the way a tokenizer does: words keep their leading space, long words break in two. */ function tokenize(text: string) { return (text.match(/\s?[\w']+|\s?[^\w\s]+|\s+/g) ?? []).flatMap((part) => { const word = part.trim(); if (word.length < 8 || !/^\w+$/.test(word)) return [part]; const cut = part.length - Math.ceil(word.length / 2); return [part.slice(0, cut), part.slice(cut)]; }); } /** The text as tokens: `tokens` when they spell out `text`, otherwise split here, with each entity kept whole. */ function toTokens(text: string | undefined, tokens: XrayLensToken[] | undefined, entities: XrayLensEntity[] | undefined) { const given = tokens?.map((t): Token => (typeof t === "string" ? { text: t } : t)); if (given && (text === undefined || given.map((t) => t.text).join("") === text)) return given; const out: Token[] = []; let rest = text ?? ""; for (const entity of entities ?? []) { const at = rest.indexOf(entity.text); if (at === -1 || !entity.text) continue; out.push(...tokenize(rest.slice(0, at)).map((t) => ({ text: t })), { text: entity.text, label: entity.label }); rest = rest.slice(at + entity.text.length); } out.push(...tokenize(rest).map((t) => ({ text: t }))); return out; } export interface XrayLensTextProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> { /** The text, as the surface shows it. */ text?: string; /** The model's tokens. If they don't spell out `text`, the text is split roughly instead. */ tokens?: XrayLensToken[]; /** Phrases to outline and label, e.g. extracted entities. Used when the text is split here. */ entities?: XrayLensEntity[]; /** Named under the glass. Defaults to the token count. */ label?: string; /** The text's element. */ as?: SpotTag; /** Classes for the token copy's box in the xray layer. */ xrayClassName?: string; } /** * Text that turns into its tokens under the lens. The xray copy keeps the * text's classes, so it wraps exactly like the surface and each word sits * over itself. */ export function XrayLensText({ text, tokens, entities, label, as = "p", className, xrayClassName, ...props }: XrayLensTextProps) { const parts = toTokens(text, tokens, entities); // The surface: plain text, with labeled tokens as spots of their own. const surface: React.ReactNode[] = []; let run = ""; parts.forEach((t, i) => { if (!t.label) { run += t.text; return; } if (run) surface.push(run); run = ""; surface.push( <XrayLensSpot key={i} as="span" label={t.label} xray={null} xrayClassName={ENTITY_BOX}> {t.text} </XrayLensSpot>, ); }); if (run) surface.push(run); const copy = React.createElement( as, { className, style: COPY_STYLE }, parts.map((t, i) => ( <span key={i} className={t.className ?? (t.p !== undefined ? confidence(t.p) : ALTERNATE[i % 2])}> {t.text} </span> )), ); return ( <XrayLensSpot as={as} label={label ?? `${parts.length} tokens`} xray={copy} xrayClassName={xrayClassName} className={className} {...props} > {surface} </XrayLensSpot> ); } /* ── The lens ───────────────────────────────────────────── */ type Options = { mode: "hover" | "drag"; size: number | string; zoom: number; rest: [number, number] | null; autoPlay: boolean | "touch"; reduced: boolean; }; type Values = { px: MotionValue<number>; py: MotionValue<number>; sx: MotionValue<number>; sy: MotionValue<number>; radius: MotionValue<number>; lift: MotionValue<number>; flood: MotionValue<number>; }; const INTERACTIVE = "a,button,input,select,textarea,label,summary,[role=button],[contenteditable=true]"; /** * Everything per frame lives here, outside React: where the lens is headed, * the springs that take it there, the tour and the CSS variables they write. */ function createLens( els: { root: HTMLDivElement; glass: HTMLDivElement; label: HTMLDivElement; labelText: HTMLSpanElement }, values: Values, spots: Spots, initial: Options, initiallyRevealed: boolean, ) { const { root, glass, label, labelText } = els; const { px, py, sx, sy, radius, lift, flood } = values; let opts = initial; let w = root.clientWidth; let h = root.clientHeight; let d = lensSize(opts.size, w, h); let want = false; // whether the lens is open, or opening let atRest = true; // parked at `rest`, so it follows `rest` through resizes let hovering = false; let focused = false; let revealed = initiallyRevealed; let inView = false; let introduced = false; let drag: { id: number; dx: number; dy: number } | null = null; let press: { id: number; x: number; y: number } | null = null; const client = { x: 0, y: 0 }; const hoverable = window.matchMedia("(hover: hover) and (pointer: fine)"); let glide: AnimationPlaybackControls[] = []; let sizing: AnimationPlaybackControls | null = null; let lifting: AnimationPlaybackControls | null = null; let flooding: AnimationPlaybackControls | null = null; let shownLabel: string | undefined; let writtenSize = -1; let tourTimer = 0; let touring = false; let stop = 0; flood.jump(revealed ? 1 : 0); const R = () => d / 2; const restPoint = () => (opts.rest ? { x: opts.rest[0] * w, y: opts.rest[1] * h } : null); const lensX = () => (opts.reduced ? px : sx).get(); const lensY = () => (opts.reduced ? py : sy).get(); /** A client point in the frame's px, with any CSS scale divided out. */ function local(cx: number, cy: number) { const rect = root.getBoundingClientRect(); const kx = root.offsetWidth ? rect.width / root.offsetWidth : 1; const ky = root.offsetHeight ? rect.height / root.offsetHeight : 1; return { x: (cx - rect.left) / kx - root.clientLeft, y: (cy - rect.top) / ky - root.clientTop }; } function place(x: number, y: number, how: "follow" | "glide" | "jump") { x = clamp(x, 0, w); y = clamp(y, 0, h); glide.forEach((a) => a.stop()); glide = []; if (how === "jump" || opts.reduced) { // The source first: its change starts the spring, which the jump then cancels. px.jump(x); py.jump(y); sx.jump(x); sy.jump(y); } else if (how === "glide") { glide = [animate(px, x, LENS.glide), animate(py, y, LENS.glide)]; } else { px.set(x); py.set(y); } } function open(on: boolean) { const to = on ? R() : 0; want = on; sizing?.stop(); if (opts.reduced) radius.jump(to); else sizing = animate(radius, to, on ? LENS.open : LENS.close); } function setLift(on: boolean) { lifting?.stop(); if (opts.reduced) lift.jump(0); else lifting = animate(lift, on ? 1 : 0, LENS.lift); } /** Opens the lens at a point, or glides it there if it's already open. */ function openAt(x: number, y: number) { place(x, y, want ? "glide" : "jump"); open(true); } function home() { const rest = restPoint(); if (rest) { atRest = true; if (want) place(rest.x, rest.y, "glide"); else openAt(rest.x, rest.y); } else if (!focused) { open(false); } } /* The tour: the lens visits each labeled spot while nobody's using it. */ function tourAllowed() { if (opts.reduced || !inView || hovering || drag || press || focused || revealed) return false; return opts.autoPlay === true || (opts.autoPlay === "touch" && !hoverable.matches); } function stopTour() { window.clearTimeout(tourTimer); tourTimer = 0; touring = false; } function startTour(delay: number) { stopTour(); if (tourAllowed()) tourTimer = window.setTimeout(tour, delay); } function tourStops() { const points: { x: number; y: number }[] = []; // Nested spots often share a center; visit each place once. for (const p of spots.stops()) { if (!points.some((q) => Math.hypot(q.x - p.x, q.y - p.y) < R() * 0.4)) points.push(p); } return points.length ? points : WANDER.map(([fx, fy]) => ({ x: fx * w, y: fy * h })); } function tour() { tourTimer = 0; if (!tourAllowed()) { touring = false; return; } const points = tourStops(); if (touring) { stop = (stop + 1) % points.length; } else if (!want) { stop = 0; touring = true; } else { // Pick up from wherever the lens is. const x = lensX(); const y = lensY(); const away = (i: number) => Math.hypot(points[i].x - x, points[i].y - y); stop = points.reduce((best, _, i) => (away(i) < away(best) ? i : best), 0); touring = true; } atRest = false; openAt(points[stop].x, points[stop].y); tourTimer = window.setTimeout(tour, LENS.dwell); } let scheduled = false; function write() { scheduled = false; const x = lensX(); const y = lensY(); const held = lift.get(); const f = clamp(flood.get(), 0, 1); const k = R() ? clamp(radius.get() / R(), 0, 1.5) : 0; const r = Math.max(0, radius.get()) * (1 + LENS.liftGrow * held); // The flood grows the mask from the lens out past the farthest corner. const far = Math.hypot(Math.max(x, w - x), Math.max(y, h - y)) + 2; // The magnification swells in with the lens, so it seems to come into focus, and settles to 1 under the flood. const zoom = 1 + (opts.zoom - 1 + LENS.liftZoom * held) * Math.min(k, 1) * (1 - f); const gx = opts.reduced ? 0 : clamp(-sx.getVelocity() * LENS.glint, -LENS.glintMax, LENS.glintMax); const gy = opts.reduced ? 0 : clamp(-sy.getVelocity() * LENS.glint, -LENS.glintMax, LENS.glintMax); const style = root.style; if (d !== writtenSize) { style.setProperty("--lens-d", `${d}px`); writtenSize = d; } style.setProperty("--lens-x", `${x}px`); style.setProperty("--lens-y", `${y}px`); style.setProperty("--lens-r", `${r + (far - r) * f}px`); style.setProperty("--lens-z", `${zoom}`); style.setProperty("--lens-s", `${(R() ? r / R() : 0) * (1 + 0.6 * f)}`); style.setProperty("--lens-o", `${1 - clamp(f * 2.5, 0, 1)}`); style.setProperty("--lens-lift", `${held}`); style.setProperty("--lens-gx", `${gx}px`); style.setProperty("--lens-gy", `${gy}px`); // Under the flood the glass is gone: it can't be grabbed or focused. glass.style.visibility = f > 0.6 ? "hidden" : ""; const next = k > 0.6 && f < 0.02 ? spots.labelAt(x, y) : undefined; if (next !== shownLabel) { if (next) { labelText.textContent = next; if (!opts.reduced) animate(labelText, { opacity: [0, 1], y: [3, 0] }, { duration: 0.18, ease: "easeOut" }); } label.dataset.show = next ? "true" : "false"; shownLabel = next; } // The label hangs under the glass, or sits above it near the bottom edge. label.style.transform = y + r + LENS.labelGap + 28 > h ? `translate(-50%, calc(-100% - ${r + LENS.labelGap}px))` : `translate(-50%, ${r + LENS.labelGap}px)`; } const schedule = () => { if (scheduled) return; scheduled = true; frame.render(write); }; const unsubscribe = [px, py, sx, sy, radius, lift, flood].map((v) => v.on("change", schedule)); const onMove = (e: PointerEvent) => { client.x = e.clientX; client.y = e.clientY; if (drag) { if (e.pointerId !== drag.id) return; const p = local(e.clientX, e.clientY); place(p.x - drag.dx, p.y - drag.dy, "follow"); return; } if (opts.mode !== "hover" || e.pointerType === "touch") return; const p = local(e.clientX, e.clientY); if (!hovering) { hovering = true; atRest = false; stopTour(); spots.measureAll(); window.addEventListener("scroll", onScroll, { capture: true, passive: true }); if (!want) { openAt(p.x, p.y); return; } } place(p.x, p.y, "follow"); }; const onLeave = (e: PointerEvent) => { if (drag || !hovering || e.pointerType === "touch") return; hovering = false; window.removeEventListener("scroll", onScroll, { capture: true }); if (tourAllowed()) startTour(LENS.resumeMouse); else home(); }; // The page scrolled under a still pointer: keep the lens on it. const onScroll = () => { if (!hovering) return; const p = local(client.x, client.y); place(p.x, p.y, "follow"); }; const onDown = (e: PointerEvent) => { if (e.button !== 0) return; // With a mouse in hover mode the lens is already under the pointer, and clicks belong to the surface. if (opts.mode === "hover" && e.pointerType !== "touch") return; stopTour(); if (want && !revealed && glass.contains(e.target as Node)) { const p = local(e.clientX, e.clientY); e.preventDefault(); atRest = false; drag = { id: e.pointerId, dx: p.x - lensX(), dy: p.y - lensY() }; root.setPointerCapture(e.pointerId); setLift(true); return; } if ((e.target as Element).closest?.(INTERACTIVE)) return; // Wait for the release: a swipe that scrolls the page shouldn't move the lens. press = { id: e.pointerId, x: e.clientX, y: e.clientY }; }; const onUp = (e: PointerEvent) => { if (drag && e.pointerId === drag.id) { drag = null; if (root.hasPointerCapture(e.pointerId)) root.releasePointerCapture(e.pointerId); setLift(false); } else if (press && e.pointerId === press.id) { const tapped = e.type === "pointerup" && Math.hypot(e.clientX - press.x, e.clientY - press.y) < 10; press = null; if (tapped && !revealed) { const p = local(e.clientX, e.clientY); atRest = false; openAt(p.x, p.y); } } startTour(LENS.resume); }; const onKey = (e: KeyboardEvent) => { const step = e.shiftKey ? LENS.stepLarge : LENS.step; const moves: Record<string, [number, number]> = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step], }; if (e.key === "Home") { e.preventDefault(); const rest = restPoint() ?? { x: w / 2, y: h / 2 }; atRest = !!opts.rest; place(rest.x, rest.y, "glide"); return; } const move = moves[e.key]; if (!move) return; e.preventDefault(); atRest = false; place(px.get() + move[0], py.get() + move[1], "follow"); }; const onFocus = () => { focused = true; stopTour(); if (!want) { const rest = restPoint() ?? { x: w / 2, y: h / 2 }; openAt(rest.x, rest.y); } }; const onBlur = () => { focused = false; if (!hovering && !drag && !opts.rest) open(false); startTour(LENS.resume); }; const onHoverChange = () => startTour(0); root.addEventListener("pointermove", onMove); root.addEventListener("pointerleave", onLeave); root.addEventListener("pointerdown", onDown); root.addEventListener("pointerup", onUp); root.addEventListener("pointercancel", onUp); glass.addEventListener("keydown", onKey); glass.addEventListener("focus", onFocus); glass.addEventListener("blur", onBlur); hoverable.addEventListener("change", onHoverChange); const sized = new ResizeObserver(() => { const [ow, oh] = [w, h]; w = root.clientWidth; h = root.clientHeight; const size = lensSize(opts.size, w, h); if (size !== d) { d = size; if (want) { sizing?.stop(); radius.jump(R()); } } spots.measureAll(); const rest = restPoint(); if (atRest && rest) place(rest.x, rest.y, "jump"); else if (ow && oh) place((px.get() * w) / ow, (py.get() * h) / oh, "jump"); schedule(); }); sized.observe(root); // The first time the frame is in view, the lens opens at rest. The tour runs only while it's in view. const seen = new IntersectionObserver( ([entry]) => { if (!entry) return; inView = entry.isIntersecting; if (!inView) { stopTour(); return; } if (!introduced) { introduced = true; spots.measureAll(); const rest = restPoint(); if (rest && !want && !hovering) openAt(rest.x, rest.y); } if (!tourTimer && !touring) startTour(LENS.tourStart); }, { threshold: 0.35 }, ); seen.observe(root); // Web fonts can move every spot without resizing anything. document.fonts?.ready.then(() => spots.measureAll()).catch(() => {}); schedule(); return { set(next: Options) { const prev = opts; opts = next; if (prev.reduced !== next.reduced) place(px.get(), py.get(), "jump"); if (prev.size !== next.size) { d = lensSize(next.size, w, h); if (want) open(true); } const moved = prev.rest?.[0] !== next.rest?.[0] || prev.rest?.[1] !== next.rest?.[1]; if (moved && introduced && atRest && !hovering && !drag && !touring) home(); if (prev.autoPlay !== next.autoPlay || prev.reduced !== next.reduced) { if (tourAllowed()) { if (!touring) startTour(0); } else { stopTour(); } } schedule(); }, /** Floods the xray over the whole frame, or pulls it back in to the lens. `from` is where it starts when the lens is closed. */ reveal(on: boolean, from?: { x: number; y: number }) { if (on === revealed) return; revealed = on; stopTour(); if (from && radius.get() < 1) place(from.x, from.y, "jump"); flooding?.stop(); if (opts.reduced) flood.jump(on ? 1 : 0); else flooding = animate(flood, on ? 1 : 0, LENS.flood); if (!on) startTour(LENS.resume); }, destroy() { cancelFrame(write); stopTour(); unsubscribe.forEach((off) => off()); glide.forEach((a) => a.stop()); sizing?.stop(); lifting?.stop(); flooding?.stop(); root.removeEventListener("pointermove", onMove); root.removeEventListener("pointerleave", onLeave); root.removeEventListener("pointerdown", onDown); root.removeEventListener("pointerup", onUp); root.removeEventListener("pointercancel", onUp); glass.removeEventListener("keydown", onKey); glass.removeEventListener("focus", onFocus); glass.removeEventListener("blur", onBlur); hoverable.removeEventListener("change", onHoverChange); window.removeEventListener("scroll", onScroll, { capture: true }); sized.disconnect(); seen.disconnect(); }, }; } type Lens = ReturnType<typeof createLens>; const REDUCED = "(prefers-reduced-motion: reduce)"; const subscribeReduced = (onChange: () => void) => { const query = window.matchMedia(REDUCED); query.addEventListener("change", onChange); return () => query.removeEventListener("change", onChange); }; /** Reduced motion, read so the server and the first client render agree. */ function useReduced() { return React.useSyncExternalStore( subscribeReduced, () => window.matchMedia(REDUCED).matches, () => false, ); } /** Twelve ticks around the rim and a crosshair, like the scale on a loupe. */ function Reticle() { return ( <span aria-hidden className="absolute inset-0 rounded-full opacity-70 mix-blend-difference"> {Array.from({ length: 12 }, (_, i) => ( <span key={i} className="absolute inset-0" style={{ transform: `rotate(${i * 30}deg)` }}> <span className={cn( "absolute top-1.5 left-1/2 w-px -translate-x-1/2 bg-white", i % 3 === 0 ? "h-2" : "h-1", )} /> </span> ))} <span className="absolute top-1/2 left-1/2 h-px w-2 -translate-y-1/2 translate-x-1 bg-white" /> <span className="absolute top-1/2 right-1/2 h-px w-2 -translate-x-1 -translate-y-1/2 bg-white" /> <span className="absolute top-1/2 left-1/2 h-2 w-px -translate-x-1/2 translate-y-1 bg-white" /> <span className="absolute bottom-1/2 left-1/2 h-2 w-px -translate-x-1/2 -translate-y-1 bg-white" /> </span> ); } export interface XrayLensProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> { /** The polished UI. It sets the frame's size. */ surface: React.ReactNode; /** What's underneath, laid over the same box and shown only inside the lens. */ xray?: React.ReactNode; /** `"hover"`: the lens follows the pointer. `"drag"`: it stays put until it's picked up. Touch screens always drag. */ mode?: "hover" | "drag"; /** Diameter of the lens: px, or a percentage of the frame's width like `"36%"`. */ size?: number | string; /** Magnification inside the lens, around its center. `1` for none. */ zoom?: number; /** Where the lens waits, as fractions of the frame. `false` closes it when the pointer leaves. */ rest?: { x: number; y: number } | false; /** Tour the labeled spots while the frame is in view: `"touch"` on touch screens only, `true` everywhere, `false` never. */ autoPlay?: boolean | "touch"; /** Floods the xray over the whole frame. Leave unset to let the switch manage it. */ revealed?: boolean; /** Whether the xray starts out covering the frame. */ defaultRevealed?: boolean; /** Called when the switch flips the layers. */ onRevealedChange?: (revealed: boolean) => void; /** Show a two-way switch that floods the xray over the whole frame. */ toggle?: boolean; /** The switch's labels: the surface first, then the xray. */ labels?: [string, string]; /** Classes for the switch, e.g. to move it to another corner. */ toggleClassName?: string; /** Draw a crosshair and rim ticks on the glass. */ reticle?: boolean; /** Accessible name of the lens, which is focusable and moves with the arrow keys. */ lensLabel?: string; /** Classes for the wrapper around `surface`. */ surfaceClassName?: string; /** Classes for the xray layer. It defaults to a dark background, so the surface never shows through. */ xrayClassName?: string; /** Classes for the glass. */ lensClassName?: string; /** Classes for the label that names the spot under the lens. */ labelClassName?: string; } /** * A loupe over a polished UI. Inside the glass the surface peels away and * the layer underneath shows through, lined up with what it explains. */ export function XrayLens({ surface, xray, mode = "hover", size = 200, zoom = 1.06, rest = { x: 0.5, y: 0.5 }, autoPlay = "touch", revealed, defaultRevealed = false, onRevealedChange, toggle = true, labels = ["Surface", "X-ray"], toggleClassName, reticle = true, lensLabel = "Inspection lens", surfaceClassName, xrayClassName, lensClassName, labelClassName, className, style, ...props }: XrayLensProps) { const rootRef = React.useRef<HTMLDivElement>(null); const glassRef = React.useRef<HTMLDivElement>(null); const labelRef = React.useRef<HTMLDivElement>(null); const labelTextRef = React.useRef<HTMLSpanElement>(null); const lensRef = React.useRef<Lens | null>(null); const originRef = React.useRef<{ x: number; y: number } | undefined>(undefined); const [spots] = React.useState(createSpots); const reduced = useReduced(); const [own, setOwn] = React.useState(defaultRevealed); const shown = revealed ?? own; // The markup starts on this side; the lens animates every flip after it. const [initial] = React.useState(shown); const shownRef = React.useRef(shown); const px = useMotionValue(0); const py = useMotionValue(0); const sx = useSpring(px, LENS.follow); const sy = useSpring(py, LENS.follow); const radius = useMotionValue(0); const lift = useMotionValue(0); const flood = useMotionValue(0); const options: Options = { mode, size, zoom: Math.max(1, zoom), rest: rest ? [clamp(rest.x, 0, 1), clamp(rest.y, 0, 1)] : null, autoPlay, reduced, }; const optionsRef = React.useRef(options); React.useLayoutEffect(() => { optionsRef.current = options; lensRef.current?.set(options); }); React.useEffect(() => { const root = rootRef.current; const glass = glassRef.current; const label = labelRef.current; const labelText = labelTextRef.current; if (!root || !glass || !label || !labelText) return; spots.connect(root); const lens = createLens( { root, glass, label, labelText }, { px, py, sx, sy, radius, lift, flood }, spots, optionsRef.current, shownRef.current, ); lensRef.current = lens; return () => { lens.destroy(); lensRef.current = null; spots.connect(null); }; }, [spots, px, py, sx, sy, radius, lift, flood]); React.useEffect(() => { shownRef.current = shown; lensRef.current?.reveal(shown, originRef.current); originRef.current = undefined; }, [shown]); const choose = (next: boolean, event: React.MouseEvent<HTMLButtonElement>) => { if (next === shown) return; const root = rootRef.current; if (root) { // Flood from the button that was pressed, if the lens is closed. const frameRect = root.getBoundingClientRect(); const button = event.currentTarget.getBoundingClientRect(); const kx = root.offsetWidth ? frameRect.width / root.offsetWidth : 1; const ky = root.offsetHeight ? frameRect.height / root.offsetHeight : 1; originRef.current = { x: (button.left + button.width / 2 - frameRect.left) / kx - root.clientLeft, y: (button.top + button.height / 2 - frameRect.top) / ky - root.clientTop, }; } if (revealed === undefined) setOwn(next); onRevealedChange?.(next); }; const disc: React.CSSProperties = { width: "var(--lens-d)", height: "var(--lens-d)", left: "calc(var(--lens-d) / -2)", top: "calc(var(--lens-d) / -2)", }; return ( <div ref={rootRef} data-xray-lens="" data-revealed={shown ? "true" : "false"} className={cn("relative isolate", mode === "hover" && "pointer-fine:cursor-crosshair", className)} style={ { "--lens-d": typeof size === "number" ? `${size}px` : "0px", "--lens-x": "0px", "--lens-y": "0px", "--lens-r": initial ? "200vmax" : "0px", "--lens-z": "1", "--lens-s": "0", "--lens-o": "1", "--lens-lift": "0", "--lens-gx": "0px", "--lens-gy": "0px", ...style, } as React.CSSProperties } {...props} > <XrayLensContext.Provider value={spots}> {/* Under the flood the surface can't be seen, so it can't be clicked either. */} <div className={cn("relative z-0 h-full", shown && "pointer-events-none", surfaceClassName)}>{surface}</div> {/* The xray: the same box as the surface, cut to a circle around the lens and magnified about its center. */} <div aria-hidden inert className="pointer-events-none absolute inset-0 z-[1] overflow-hidden rounded-[inherit] select-none" style={{ maskImage: "radial-gradient(circle var(--lens-r) at var(--lens-x) var(--lens-y), #000 calc(100% - 1px), transparent 100%)", WebkitMaskImage: "radial-gradient(circle var(--lens-r) at var(--lens-x) var(--lens-y), #000 calc(100% - 1px), transparent 100%)", }} > <div className={cn("absolute inset-0 bg-zinc-950 text-zinc-100", xrayClassName)} style={{ transformOrigin: "var(--lens-x) var(--lens-y)", transform: "scale(var(--lens-z))" }} > {xray} <SpotLayer spots={spots} /> </div> </div> {/* The glass, centered on the lens. */} <div className="pointer-events-none absolute top-0 left-0 z-[2]" style={{ transform: "translate3d(var(--lens-x), var(--lens-y), 0)" }} > <div aria-hidden className="absolute rounded-full shadow-[0_22px_44px_-16px_rgb(0_0_0/0.5),0_6px_14px_-6px_rgb(0_0_0/0.3)]" style={{ ...disc, opacity: "var(--lens-o)", transform: `translateY(calc(var(--lens-lift) * ${LENS.liftDrop}px)) scale(var(--lens-s))`, }} /> <div ref={glassRef} tabIndex={0} role="group" aria-roledescription="lens" aria-label={lensLabel} aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight Home" className={cn( "absolute touch-none rounded-full ring-1 ring-black/25 outline-none focus-visible:ring-2 focus-visible:ring-sky-400 dark:ring-white/20", mode === "drag" ? "pointer-events-auto cursor-grab active:cursor-grabbing" : "pointer-coarse:pointer-events-auto", lensClassName, )} style={{ ...disc, opacity: "var(--lens-o)", transform: "scale(var(--lens-s))" }} > {/* The edge of a thick lens: a bright rim, a dark band, then a falloff toward the middle. */} <span className="absolute inset-0 rounded-full shadow-[inset_0_0_0_1px_rgb(255_255_255/0.55),inset_0_0_0_3px_rgb(0_0_0/0.3),inset_0_0_26px_rgb(0_0_0/0.45)]" /> {/* A trace of color fringing just inside the rim. */} <span className="absolute inset-[3px] rounded-full shadow-[inset_1.5px_1px_0_rgb(56_189_248/0.3),inset_-1.5px_-1px_0_rgb(244_114_182/0.25)]" /> <span className="absolute inset-0 rounded-full bg-[linear-gradient(155deg,rgb(255_255_255/0.16),rgb(255_255_255/0)_38%,rgb(255_255_255/0)_72%,rgb(255_255_255/0.06))]" /> <span className="absolute top-[9%] left-[16%] h-[12%] w-[36%]" style={{ transform: "translate(var(--lens-gx), var(--lens-gy))" }} > <span className="block size-full -rotate-[32deg] rounded-full bg-[radial-gradient(closest-side,rgb(255_255_255/0.55),rgb(255_255_255/0))]" /> </span> {reticle && <Reticle />} </div> <div ref={labelRef} aria-hidden data-show="false" className={cn( "absolute top-0 left-0 flex items-center gap-1.5 rounded-full bg-zinc-950/90 px-2.5 py-1 font-mono text-[11px] leading-4 font-medium whitespace-nowrap text-white opacity-0 shadow-lg ring-1 ring-white/10 backdrop-blur-sm transition-opacity duration-200 data-[show=true]:opacity-100", labelClassName, )} > <span className="size-1.5 rounded-full bg-emerald-400" /> <span ref={labelTextRef} /> </div> </div> {toggle && ( <div role="group" aria-label="Layers" className={cn( "absolute right-3 bottom-3 z-[3] grid cursor-default grid-cols-2 rounded-full bg-zinc-950/80 p-1 text-xs font-medium text-white shadow-lg ring-1 ring-white/15 backdrop-blur-md", toggleClassName, )} > <span aria-hidden className={cn( "absolute inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-full bg-white shadow-sm transition-transform duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] motion-reduce:transition-none", shown && "translate-x-full", )} /> {labels.map((text, i) => { const active = shown === (i === 1); return ( <button key={i} type="button" aria-pressed={active} onClick={(event) => choose(i === 1, event)} className={cn( "relative cursor-pointer rounded-full px-3 py-1.5 whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:ring-white/70", active ? "text-zinc-950" : "text-white/75 hover:text-white", )} > {text} </button> ); })} </div> )} </XrayLensContext.Provider> </div> ); }Update the import paths
The file imports
cnfrom@/lib/utils. Change the path if your project uses a different alias.
Examples
Finished sections built with Xray Lens. Each one installs whole, with the component included.
Blueprint
A studio's booking card over its own blueprint: component names, type and spacing tokens on wireframe bounds. `mode="drag"` with more `zoom`; pick the lens up and it lifts. The Design/Blueprint switch sits over the photo.
Design systems
Built from parts, not pixels.
Every screen we ship is made of named components and tokens your team can reuse. Pick up the loupe and drag it across the card to see the system underneath.
Timber House
Lake Bohinj, Slovenia
import type { ReactNode } from "react";
import { XrayLens, XrayLensSpot } from "@/components/ui/xray-lens";
const photo =
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=800&h=600&q=80";
// Blueprint lines: each part of the card drawn as a thin white box.
const line = "outline-1 -outline-offset-1 outline-white/60";
function Part({ name, spec }: { name: string; spec?: string }) {
return (
<span className="flex h-full flex-col justify-center gap-0.5 px-1.5 font-mono text-[10px] leading-tight whitespace-nowrap">
<span className="text-white">{name}</span>
{spec && <span className="text-sky-200/70">{spec}</span>}
</span>
);
}
function Tag({ children }: { children: ReactNode }) {
return (
<XrayLensSpot label="Tag" xrayClassName={line} xray={<Part name="<Tag>" />}>
<span className="inline-flex h-6 items-center rounded-md bg-zinc-100 px-2 text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
{children}
</span>
</XrayLensSpot>
);
}
function Listing() {
return (
<div className="p-3">
<XrayLensSpot
label="Image · 4:3 · radius 12"
xrayClassName={line}
xray={
<span className="relative block size-full">
<svg aria-hidden className="absolute inset-0 size-full text-white/35" preserveAspectRatio="none" viewBox="0 0 100 100">
<line x1="0" y1="0" x2="100" y2="100" stroke="currentColor" vectorEffect="non-scaling-stroke" />
<line x1="100" y1="0" x2="0" y2="100" stroke="currentColor" vectorEffect="non-scaling-stroke" />
</svg>
<span className="absolute inset-0 grid place-items-center">
<span className="bg-[#0b3c8f] px-1.5 py-0.5">
<Part name="<Image ratio="4/3" />" spec="cover · radius 12" />
</span>
</span>
</span>
}
>
{/* eslint-disable-next-line @next/next/no-img-element -- a remote photo in an installable demo */}
<img
src={photo}
alt="A low timber and glass house lit up at dusk"
draggable={false}
className="aspect-4/3 w-full rounded-xl bg-zinc-100 object-cover dark:bg-zinc-900"
/>
</XrayLensSpot>
<div className="flex flex-col gap-3 px-1.5 pt-4 pb-1.5">
<div className="flex items-start justify-between gap-3">
<XrayLensSpot
label="Heading · 17/24 · 600"
xrayClassName={line}
xray={<Part name="<Heading size="md">" spec="17/24 · 600 · -1%" />}
>
<h4 className="text-[17px] leading-6 font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
Timber House
</h4>
<p className="text-sm text-zinc-500">Lake Bohinj, Slovenia</p>
</XrayLensSpot>
<XrayLensSpot
label="Rating"
xrayClassName={line}
xray={<Part name="<Rating>" spec="value 4.92" />}
className="flex items-center gap-1 pt-0.5 text-sm font-medium text-zinc-900 tabular-nums dark:text-zinc-100"
>
<svg aria-hidden viewBox="0 0 20 20" className="size-3.5 fill-amber-400">
<path d="M10 1.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L10 14.9l-5.2 2.7 1-5.8L1.5 7.7l5.9-.9z" />
</svg>
4.92
</XrayLensSpot>
</div>
<XrayLensSpot
label="Stack · row · gap 6"
xrayClassName={line}
xray={null}
className="flex flex-wrap gap-1.5"
>
<Tag>Sauna</Tag>
<Tag>Lake view</Tag>
<Tag>Sleeps 4</Tag>
</XrayLensSpot>
<div className="mt-1 flex items-center justify-between border-t border-zinc-100 pt-3 dark:border-zinc-800">
<XrayLensSpot
label="Price · tabular"
xrayClassName={line}
xray={<Part name="<Price unit="night">" spec="EUR · tabular" />}
className="text-sm text-zinc-500"
>
<span className="text-base font-semibold text-zinc-900 tabular-nums dark:text-zinc-50">€184</span>{" "}
night
</XrayLensSpot>
<XrayLensSpot
label="Button · md · h 36"
xrayClassName={line}
xray={<Part name="<Button>" spec="md · h 36 · px 14" />}
>
<button
type="button"
className="h-9 rounded-lg bg-zinc-900 px-3.5 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
Reserve
</button>
</XrayLensSpot>
</div>
</div>
</div>
);
}
export function XrayLensBlueprintDemo() {
return (
<section className="grid w-full max-w-3xl items-center gap-10 sm:grid-cols-[minmax(0,1fr)_20rem]">
<div>
<p className="font-mono text-xs tracking-widest text-zinc-500 uppercase">Design systems</p>
<h3 className="mt-3 text-3xl font-semibold tracking-tight text-balance text-zinc-900 dark:text-zinc-50">
Built from parts, not pixels.
</h3>
<p className="mt-4 max-w-sm text-sm/6 text-pretty text-zinc-600 dark:text-zinc-400">
Every screen we ship is made of named components and tokens your team can reuse. Pick up
the loupe and drag it across the card to see the system underneath.
</p>
</div>
<XrayLens
mode="drag"
size="55%"
zoom={1.14}
rest={{ x: 0.45, y: 0.68 }}
lensLabel="Blueprint lens"
labels={["Design", "Blueprint"]}
toggleClassName="top-5 right-5 bottom-auto"
surface={<Listing />}
xray={
<div className="absolute inset-0 bg-[linear-gradient(rgb(255_255_255/0.07)_1px,transparent_1px),linear-gradient(90deg,rgb(255_255_255/0.07)_1px,transparent_1px)] bg-size-[8px_8px]">
<div className="absolute inset-0 bg-[linear-gradient(rgb(255_255_255/0.08)_1px,transparent_1px),linear-gradient(90deg,rgb(255_255_255/0.08)_1px,transparent_1px)] bg-size-[40px_40px]" />
</div>
}
xrayClassName="bg-[#0b3c8f] text-white"
labelClassName="bg-[#0b3c8f]/95 ring-white/25"
className="w-full rounded-2xl bg-white shadow-[0_1px_2px_rgb(0_0_0/0.05),0_16px_40px_-20px_rgb(0_0_0/0.3)] ring-1 ring-zinc-950/5 dark:bg-zinc-950 dark:ring-white/10"
/>
</section>
);
}
Request Trace
An AI search answer over the live trace that produced it: retrieval, generation and citation spans on the request's timeline, and the answer recolored by when each word streamed. `autoPlay` tours the spans on desktop too until the pointer takes over, and `rest={false}` keeps the lens closed before the tour starts.
Observability
Every answer, traced to the millisecond.
Create the new key and deploy it alongside the old one. Once traffic has moved over, revoke the old key1. Both keys stay valid during the overlap, so no request fails2.
docs.acme.dev
Rotating API keys
acme.dev/blog
Zero-downtime deploys
status.acme.dev
Incident 4127
The lens tours each span. Point at any part of the answer to take over.
"use client";
import * as React from "react";
import { XrayLens, XrayLensSpot } from "@/components/ui/xray-lens";
// One trace, in the order it ran. Each span is drawn over the part of the answer it produced.
const TRACE = [
{ id: "embed", ms: 18 },
{ id: "retrieve", ms: 42 },
{ id: "rerank", ms: 31 },
{ id: "generate", ms: 812 },
{ id: "cite", ms: 12 },
{ id: "suggest", ms: 64 },
] as const;
type SpanId = (typeof TRACE)[number]["id"];
type At = { start: number; ms: number };
type Timing = Record<SpanId, At>;
const BASE = Object.fromEntries(TRACE.map((s) => [s.id, s.ms])) as Record<SpanId, number>;
function lay(durations: Record<SpanId, number>) {
let t = 0;
const timing = {} as Timing;
for (const { id } of TRACE) {
timing[id] = { start: t, ms: durations[id] };
t += durations[id];
}
return { timing, total: t };
}
/** The trace re-measured every couple of seconds, as a live dashboard would show it. */
function useLiveTrace() {
const [durations, setDurations] = React.useState(BASE);
React.useEffect(() => {
const id = window.setInterval(() => {
setDurations(
Object.fromEntries(
TRACE.map((s) => [s.id, Math.round(s.ms * (0.88 + Math.random() * 0.24))]),
) as Record<SpanId, number>,
);
}, 1600);
return () => window.clearInterval(id);
}, []);
return lay(durations);
}
const spanBox = "outline-1 -outline-offset-1 outline-emerald-400/40 bg-emerald-400/[0.06]";
/** Where a span sits on the whole request's timeline. */
function Bar({ at, total }: { at: At; total: number }) {
return (
<span className="relative h-1 min-w-8 flex-1 rounded-full bg-white/10">
<span
className="absolute inset-y-0 min-w-[3px] rounded-full bg-emerald-400 transition-[left,width] duration-500"
style={{ left: `${(at.start / total) * 100}%`, width: `${(at.ms / total) * 100}%` }}
/>
</span>
);
}
/** A span in a box: its name and time, its place on the timeline, and a note. */
function Span({ name, at, total, note }: { name: string; at: At; total: number; note?: string }) {
return (
<span className="flex h-full min-w-0 flex-col justify-center gap-1.5 px-2.5 font-mono text-[10.5px] leading-none">
<span className="flex items-center justify-between gap-3 whitespace-nowrap">
<span className="text-emerald-300">{name}</span>
<span className="text-zinc-100 tabular-nums">{at.ms}ms</span>
</span>
<span className="flex">
<Bar at={at} total={total} />
</span>
{note && <span className="truncate text-zinc-500">{note}</span>}
</span>
);
}
/** A span on one line, for the section labels. */
function SpanRow({ name, at, total }: { name: string; at: At; total: number }) {
return (
<span className="flex h-full items-center gap-2.5 font-mono text-[10px] leading-none whitespace-nowrap">
<span className="text-emerald-300">{name}</span>
<Bar at={at} total={total} />
<span className="text-zinc-100 tabular-nums">{at.ms}ms</span>
</span>
);
}
// The answer as text and citations. The surface and the xray both render it from
// here, so they wrap the same way.
const ANSWER: (string | number)[] = [
"Create the new key and deploy it alongside the old one. Once traffic has moved over, revoke the old key",
1,
". Both keys stay valid during the overlap, so no request fails",
2,
".",
];
const WORDS = ANSWER.filter((part) => typeof part === "string").join(" ").split(/\s+/).length;
const answer = "text-[15px] leading-relaxed text-pretty";
const cite =
"mx-0.5 inline-grid size-[18px] place-items-center rounded-full align-[2px] text-[10px] font-semibold";
/** The answer again, each word colored by when it streamed in: green first, violet last. */
function Streamed() {
let n = 0;
return (
<p className={answer}>
{ANSWER.map((part, i) =>
typeof part === "number" ? (
<span key={i} className={`${cite} text-transparent`}>
{part}
</span>
) : (
part.split(/(\s+)/).map((word, j) =>
/\s/.test(word) || !word ? (
word
) : (
<span key={`${i}-${j}`} style={{ color: `oklch(0.82 0.14 ${160 + (n++ / WORDS) * 140})` }}>
{word}
</span>
),
)
),
)}
</p>
);
}
const SOURCES = [
{ host: "docs.acme.dev", title: "Rotating API keys", score: "0.91" },
{ host: "acme.dev/blog", title: "Zero-downtime deploys", score: "0.88" },
{ host: "status.acme.dev", title: "Incident 4127", score: "0.64" },
];
function Answer({ timing, total }: { timing: Timing; total: number }) {
return (
<div className="flex flex-col gap-5 p-5 sm:p-6">
<XrayLensSpot
label="embed · query"
xrayClassName={spanBox}
xray={<Span name="embed" at={timing.embed} total={total} note="trace 7f3a2c · 1536 dims" />}
className="flex h-12 items-center gap-2.5 rounded-xl bg-zinc-100 px-3.5 text-sm text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
>
<svg aria-hidden viewBox="0 0 20 20" className="size-4 shrink-0 fill-none stroke-zinc-400 stroke-2">
<circle cx="9" cy="9" r="6" />
<path d="M13.5 13.5 17 17" strokeLinecap="round" />
</svg>
<span className="truncate">How do I rotate API keys without downtime?</span>
</XrayLensSpot>
<div>
<XrayLensSpot
label="generate"
xray={<SpanRow name="generate" at={timing.generate} total={total} />}
className="text-xs leading-4 font-medium tracking-wider text-zinc-500 uppercase"
>
Answer
</XrayLensSpot>
<XrayLensSpot
label={`stream · 214 tokens · first at ${Math.round(timing.generate.ms * 0.22)}ms`}
xray={<Streamed />}
className="mt-2"
>
<p className={`${answer} text-zinc-700 dark:text-zinc-300`}>
{ANSWER.map((part, i) =>
typeof part === "number" ? (
<XrayLensSpot
key={i}
as="span"
label={`cite · doc_${part} · ${timing.cite.ms}ms`}
xrayClassName="outline-1 -outline-offset-1 outline-amber-300/80 bg-amber-300/15"
xray={
<span className="grid size-full place-items-center font-mono text-[9px] text-amber-200">
d{part}
</span>
}
className={`${cite} bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300`}
>
{part}
</XrayLensSpot>
) : (
part
),
)}
</p>
</XrayLensSpot>
</div>
<div>
<XrayLensSpot
label="retrieve"
xray={<SpanRow name="retrieve" at={timing.retrieve} total={total} />}
className="text-xs leading-4 font-medium tracking-wider text-zinc-500 uppercase"
>
Sources
</XrayLensSpot>
<div className="mt-2 grid gap-2 sm:grid-cols-3">
{SOURCES.map((source, i) => (
<XrayLensSpot
key={source.host}
label={`${i === 2 ? "rerank" : "retrieve"} · doc_${i + 1}`}
xrayClassName={spanBox}
xray={
<Span
name={i === 2 ? "rerank" : `doc_${i + 1}`}
at={i === 2 ? timing.rerank : timing.retrieve}
total={total}
note={`score ${source.score}${i === 2 ? " · dropped" : ""}`}
/>
}
className="rounded-lg p-2.5 ring-1 ring-zinc-950/10 dark:ring-white/10"
>
<p className="truncate text-[11px] text-zinc-500">{source.host}</p>
<p className="mt-0.5 truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">
{source.title}
</p>
</XrayLensSpot>
))}
</div>
</div>
<XrayLensSpot
label="suggest · 2 follow-ups"
xrayClassName={spanBox}
xray={<SpanRow name="suggest" at={timing.suggest} total={total} />}
className="flex flex-wrap gap-2 self-start"
>
{["Revoke a leaked key", "Scope a key to one project"].map((q) => (
<button
key={q}
type="button"
className="h-8 rounded-full px-3 text-xs text-zinc-600 ring-1 ring-zinc-950/10 transition-colors hover:bg-zinc-50 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-zinc-900"
>
{q}
</button>
))}
</XrayLensSpot>
</div>
);
}
export function XrayLensTraceDemo() {
const { timing, total } = useLiveTrace();
return (
<section className="w-full max-w-2xl">
<div className="mb-8 text-center">
<p className="font-mono text-xs tracking-widest text-zinc-500 uppercase">Observability</p>
<h3 className="mt-3 text-3xl font-semibold tracking-tight text-balance text-zinc-900 dark:text-zinc-50">
Every answer, traced to the millisecond.
</h3>
</div>
<XrayLens
rest={false}
size="28%"
autoPlay
zoom={1.08}
lensLabel="Trace lens"
labels={["Answer", "Trace"]}
toggleClassName="top-full right-auto bottom-auto left-1/2 mt-4 -translate-x-1/2"
surface={<Answer timing={timing} total={total} />}
xray={
<div className="absolute inset-0 bg-[linear-gradient(90deg,rgb(52_211_153/0.07)_1px,transparent_1px)] bg-size-[24px_100%]" />
}
xrayClassName="bg-[#050807] text-zinc-100"
className="rounded-2xl bg-white shadow-[0_1px_2px_rgb(0_0_0/0.05),0_16px_40px_-20px_rgb(0_0_0/0.3)] ring-1 ring-zinc-950/5 dark:bg-zinc-950 dark:ring-white/10"
/>
<p className="mt-20 text-center text-sm text-zinc-500">
The lens tours each span. Point at any part of the answer to take over.
</p>
</section>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
surfaceRequired | React.ReactNode | None | The polished UI. It sets the frame's size. Mark parts of it with XrayLensSpot to give them an xray of their own. |
xray | React.ReactNode | None | What's underneath, laid over the same box and shown only inside the lens: a grid, a code dump, a schema. Spots draw on top of it. |
mode | "hover" | "drag" | "hover" | hover: the lens flies to the pointer and trails it. drag: it stays put until it's picked up, and a click elsewhere glides it there. Touch screens always drag. |
size | number | string | 200 | Diameter of the lens: px, or a percentage of the frame's width like "36%", so it shrinks with the card on phones. It never grows past 90% of the frame's shorter side. |
zoom | number | 1.06 | Magnification inside the lens, around its center, so whatever sits under the crosshair stays lined up. 1 for none. |
rest | { x: number; y: number } | false | { x: 0.5, y: 0.5 } | Where the lens waits, as fractions of the frame. It opens there when the frame scrolls into view and glides back when the pointer leaves. false keeps it closed until the pointer arrives. |
autoPlay | boolean | "touch" | "touch" | Let the lens tour the labeled spots by itself while the frame is in view, pausing on each so its label shows: "touch" on touch screens only, true everywhere, false never. A finger, the pointer or focus takes over; the tour picks up again from the nearest spot. Never with reduced motion. |
toggle | boolean | true | Show a two-way switch that floods the xray over the whole frame, from the lens or from the switch, so the layer underneath never depends on hover. The magnification settles to 1 under the flood, so everything lines up. |
labels | [string, string] | ["Surface", "X-ray"] | The switch's labels: the surface first, then the xray. |
toggleClassName | string | None | Classes for the switch. It sits in the bottom-right corner; on a dense card, move it under the frame with top-full right-auto bottom-auto left-1/2 mt-4 -translate-x-1/2. |
revealed | boolean | None | Floods the xray over the whole frame. Leave unset to let the switch manage it. |
defaultRevealed | boolean | false | Whether the xray starts out covering the frame. |
onRevealedChange | (revealed: boolean) => void | None | Called when the switch flips the layers. |
reticle | boolean | true | Draw a crosshair and rim ticks on the glass. |
lensLabel | string | "Inspection lens" | Accessible name of the lens. It's focusable: arrow keys move it (Shift for bigger steps) and Home sends it back to rest. |
className | string | None | Classes for the frame. Put the card's radius here so the xray layer follows it. The frame sets --lens-x, --lens-y and --lens-r (px) every frame, so content in either layer can react to the lens. |
surfaceClassName | string | None | Classes for the wrapper around surface. |
xrayClassName | string | None | Classes for the xray layer. It defaults to bg-zinc-950 text-zinc-100, so the surface never shows through. |
lensClassName | string | None | Classes for the glass, e.g. a different ring-* color for the rim. |
labelClassName | string | None | Classes for the label that names the spot under the lens. |
spot.xrayRequired | React.ReactNode | None | XrayLensSpot: what this element looks like under the lens. It's drawn in the xray layer in a box with the element's exact position, size and corner radius, and follows it through layout changes. |
spot.label | string | None | XrayLensSpot: named under the glass while the lens is centered on this element. The smallest spot under the center wins. |
spot.xrayClassName | string | None | XrayLensSpot: classes for the spot's box in the xray layer, e.g. a dashed outline. |
spot.as | "div" | "span" | "div" | XrayLensSpot: the wrapper element. Use span (with inline-block) to mark words inside a paragraph. |
spot.className | string | None | XrayLensSpot: classes for the wrapper in the surface. |
text.text | string | None | XrayLensText: the text as the surface shows it. Under the lens it turns into its tokens in place: the copy keeps the text's classes, so it wraps exactly the same way. |
text.tokens | (string | { text: string; p?: number; label?: string; className?: string })[] | None | XrayLensText: your model's tokens. With p (probability, 0–1) each token is tinted by confidence: green, amber below 0.85, red below 0.5. A label makes the token a spot. If they don't spell out text, the text is split roughly instead. |
text.entities | { text: string; label: string }[] | None | XrayLensText: phrases to outline and label, such as entities the model extracted. Used when the text is split here rather than given as tokens. |
text.label | string | None | XrayLensText: named under the glass. Defaults to the token count. |
text.as | "p" | "span" | "div" | "li" | "blockquote" | "p" | XrayLensText: the text's element. Its className styles both the text and its token copy, so keep colors that differ out of it or let the tokens override them. |
surfaceRequiredType
React.ReactNodeThe polished UI. It sets the frame's size. Mark parts of it with
XrayLensSpotto give them an xray of their own.xrayType
React.ReactNodeWhat's underneath, laid over the same box and shown only inside the lens: a grid, a code dump, a schema. Spots draw on top of it.
modeType
"hover" | "drag"Default"hover"hover: the lens flies to the pointer and trails it.drag: it stays put until it's picked up, and a click elsewhere glides it there. Touch screens always drag.sizeType
number | stringDefault200Diameter of the lens: px, or a percentage of the frame's width like
"36%", so it shrinks with the card on phones. It never grows past 90% of the frame's shorter side.zoomType
numberDefault1.06Magnification inside the lens, around its center, so whatever sits under the crosshair stays lined up.
1for none.restType
{ x: number; y: number } | falseDefault{ x: 0.5, y: 0.5 }Where the lens waits, as fractions of the frame. It opens there when the frame scrolls into view and glides back when the pointer leaves.
falsekeeps it closed until the pointer arrives.autoPlayType
boolean | "touch"Default"touch"Let the lens tour the labeled spots by itself while the frame is in view, pausing on each so its label shows:
"touch"on touch screens only,trueeverywhere,falsenever. A finger, the pointer or focus takes over; the tour picks up again from the nearest spot. Never with reduced motion.toggleType
booleanDefaulttrueShow a two-way switch that floods the xray over the whole frame, from the lens or from the switch, so the layer underneath never depends on hover. The magnification settles to 1 under the flood, so everything lines up.
labelsType
[string, string]Default["Surface", "X-ray"]The switch's labels: the surface first, then the xray.
toggleClassNameType
stringClasses for the switch. It sits in the bottom-right corner; on a dense card, move it under the frame with
top-full right-auto bottom-auto left-1/2 mt-4 -translate-x-1/2.revealedType
booleanFloods the xray over the whole frame. Leave unset to let the switch manage it.
defaultRevealedType
booleanDefaultfalseWhether the xray starts out covering the frame.
onRevealedChangeType
(revealed: boolean) => voidCalled when the switch flips the layers.
reticleType
booleanDefaulttrueDraw a crosshair and rim ticks on the glass.
lensLabelType
stringDefault"Inspection lens"Accessible name of the lens. It's focusable: arrow keys move it (Shift for bigger steps) and Home sends it back to
rest.classNameType
stringClasses for the frame. Put the card's radius here so the xray layer follows it. The frame sets
--lens-x,--lens-yand--lens-r(px) every frame, so content in either layer can react to the lens.surfaceClassNameType
stringClasses for the wrapper around
surface.xrayClassNameType
stringClasses for the xray layer. It defaults to
bg-zinc-950 text-zinc-100, so the surface never shows through.lensClassNameType
stringClasses for the glass, e.g. a different
ring-*color for the rim.labelClassNameType
stringClasses for the label that names the spot under the lens.
spot.xrayRequiredType
React.ReactNodeXrayLensSpot: what this element looks like under the lens. It's drawn in the xray layer in a box with the element's exact position, size and corner radius, and follows it through layout changes.spot.labelType
stringXrayLensSpot: named under the glass while the lens is centered on this element. The smallest spot under the center wins.spot.xrayClassNameType
stringXrayLensSpot: classes for the spot's box in the xray layer, e.g. a dashed outline.spot.asType
"div" | "span"Default"div"XrayLensSpot: the wrapper element. Usespan(withinline-block) to mark words inside a paragraph.spot.classNameType
stringXrayLensSpot: classes for the wrapper in the surface.text.textType
stringXrayLensText: the text as the surface shows it. Under the lens it turns into its tokens in place: the copy keeps the text's classes, so it wraps exactly the same way.text.tokensType
(string | { text: string; p?: number; label?: string; className?: string })[]XrayLensText: your model's tokens. Withp(probability, 0–1) each token is tinted by confidence: green, amber below 0.85, red below 0.5. Alabelmakes the token a spot. If they don't spell outtext, the text is split roughly instead.text.entitiesType
{ text: string; label: string }[]XrayLensText: phrases to outline and label, such as entities the model extracted. Used when the text is split here rather than given astokens.text.labelType
stringXrayLensText: named under the glass. Defaults to the token count.text.asType
"p" | "span" | "div" | "li" | "blockquote"Default"p"XrayLensText: the text's element. ItsclassNamestyles both the text and its token copy, so keep colors that differ out of it or let the tokens override them.