X-Ray
import { useRef } from 'react';
const css = `
.card-x-ray {
--x: -100px;
--y: -100px;
position: relative;
width: 210px;
height: 176px;
cursor: none;
}
.card-x-ray__face {
position: absolute;
inset: 0;
display: grid;
grid-template-rows: 74px auto auto 1fr;
gap: 6px;
padding: 12px;
border-radius: 16px;
}
.card-x-ray__face--done { background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-x-ray__img { border-radius: 10px; background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#dbdde1, #2e3035)); }
.card-x-ray__title { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-x-ray__body { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.card-x-ray__btn { justify-self: start; align-self: end; padding: 5px 12px; border-radius: 999px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); font-size: 11px; font-weight: 600; }
/* The same layout as a wireframe, shown only inside the lens. */
.card-x-ray__face--wire {
background: light-dark(#fbfcfd, #121417);
background-image: linear-gradient(light-dark(#ebedf0, #232529) 1px, transparent 1px), linear-gradient(90deg, light-dark(#ebedf0, #232529) 1px, transparent 1px);
background-size: 12px 12px;
box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087);
clip-path: circle(0 at var(--x) var(--y));
transition: clip-path 0.25s ease;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 9px;
color: light-dark(#50535b, #c6c8cd);
}
.card-x-ray:hover .card-x-ray__face--wire { clip-path: circle(46px at var(--x) var(--y)); transition: none; }
.card-x-ray__face--wire > * {
display: grid;
place-items: center;
border: 1px dashed light-dark(#5f626a, #a7abb2);
border-radius: 6px;
background: none;
color: light-dark(#50535b, #c6c8cd);
font-weight: 400;
}
.card-x-ray__face--wire .card-x-ray__title, .card-x-ray__face--wire .card-x-ray__body { place-items: center start; padding: 1px 6px; }
.card-x-ray__lens {
position: absolute;
left: var(--x);
top: var(--y);
width: 92px;
height: 92px;
margin: -46px;
border-radius: 50%;
box-shadow: 0 0 0 1.5px light-dark(#42454c, #e5e6e9), 0 0 0 5px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.card-x-ray:hover .card-x-ray__lens { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.card-x-ray__face--wire, .card-x-ray__lens { transition: none; }
}
`;
export function XRayCard() {
const root = useRef<HTMLDivElement>(null);
return (
<>
<style>
{css}
</style>
<div
ref={root}
className="card-x-ray"
role="group"
aria-label="Field notes"
onPointerMove={(e) => {
const r = root.current!.getBoundingClientRect();
root.current!.style.setProperty('--x', `${e.clientX - r.left}px`);
root.current!.style.setProperty('--y', `${e.clientY - r.top}px`);
}}
>
<div className="card-x-ray__face card-x-ray__face--done">
<span className="card-x-ray__img" aria-hidden="true" />
<span className="card-x-ray__title">Field notes</span>
<span className="card-x-ray__body">Sketches from the ridge walk</span>
<span className="card-x-ray__btn">Read</span>
</div>
<div className="card-x-ray__face card-x-ray__face--wire" aria-hidden="true">
<span className="card-x-ray__img">img 186 × 74</span>
<span className="card-x-ray__title">h3 · 13/600</span>
<span className="card-x-ray__body">p · 11/400</span>
<span className="card-x-ray__btn">button</span>
</div>
<span className="card-x-ray__lens" aria-hidden="true" />
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardXRay(el) on the mounted element instead.
<div class="card-x-ray" role="group" aria-label="Field notes">
<div class="card-x-ray__face card-x-ray__face--done">
<span class="card-x-ray__img" aria-hidden="true"></span>
<span class="card-x-ray__title">Field notes</span>
<span class="card-x-ray__body">Sketches from the ridge walk</span>
<span class="card-x-ray__btn">Read</span>
</div>
<div class="card-x-ray__face card-x-ray__face--wire" aria-hidden="true">
<span class="card-x-ray__img">img 186 × 74</span>
<span class="card-x-ray__title">h3 · 13/600</span>
<span class="card-x-ray__body">p · 11/400</span>
<span class="card-x-ray__btn">button</span>
</div>
<span class="card-x-ray__lens" aria-hidden="true"></span>
</div>.card-x-ray {
--x: -100px;
--y: -100px;
position: relative;
width: 210px;
height: 176px;
cursor: none;
}
.card-x-ray__face {
position: absolute;
inset: 0;
display: grid;
grid-template-rows: 74px auto auto 1fr;
gap: 6px;
padding: 12px;
border-radius: 16px;
}
.card-x-ray__face--done { background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-x-ray__img { border-radius: 10px; background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#dbdde1, #2e3035)); }
.card-x-ray__title { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-x-ray__body { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.card-x-ray__btn { justify-self: start; align-self: end; padding: 5px 12px; border-radius: 999px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); font-size: 11px; font-weight: 600; }
/* The same layout as a wireframe, shown only inside the lens. */
.card-x-ray__face--wire {
background: light-dark(#fbfcfd, #121417);
background-image: linear-gradient(light-dark(#ebedf0, #232529) 1px, transparent 1px), linear-gradient(90deg, light-dark(#ebedf0, #232529) 1px, transparent 1px);
background-size: 12px 12px;
box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087);
clip-path: circle(0 at var(--x) var(--y));
transition: clip-path 0.25s ease;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 9px;
color: light-dark(#50535b, #c6c8cd);
}
.card-x-ray:hover .card-x-ray__face--wire { clip-path: circle(46px at var(--x) var(--y)); transition: none; }
.card-x-ray__face--wire > * {
display: grid;
place-items: center;
border: 1px dashed light-dark(#5f626a, #a7abb2);
border-radius: 6px;
background: none;
color: light-dark(#50535b, #c6c8cd);
font-weight: 400;
}
.card-x-ray__face--wire .card-x-ray__title, .card-x-ray__face--wire .card-x-ray__body { place-items: center start; padding: 1px 6px; }
.card-x-ray__lens {
position: absolute;
left: var(--x);
top: var(--y);
width: 92px;
height: 92px;
margin: -46px;
border-radius: 50%;
box-shadow: 0 0 0 1.5px light-dark(#42454c, #e5e6e9), 0 0 0 5px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.card-x-ray:hover .card-x-ray__lens { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.card-x-ray__face--wire, .card-x-ray__lens { transition: none; }
}function initCardXRay(root) {
function move(e) {
var r = root.getBoundingClientRect();
root.style.setProperty('--x', e.clientX - r.left + 'px');
root.style.setProperty('--y', e.clientY - r.top + 'px');
}
root.addEventListener('pointermove', move);
return function () { root.removeEventListener('pointermove', move); };
}
document.querySelectorAll('.card-x-ray').forEach(initCardXRay);Goal: Create a "X-Ray" card.
Visual behavior: A finished card with an x-ray lens: move the pointer over it and a round lens follows, showing the wireframe underneath, the same layout drawn as dashed boxes on a grid with each element's spec.
Technique:
- Two faces share one grid layout: the finished card, and a wireframe copy on a 12px grid where each element is a dashed box labelled with its spec.
- The wireframe is clipped with clip-path: circle(r at var(--x) var(--y)); r is 0 at rest and 46px on hover, so the lens opens and closes smoothly.
- pointermove writes --x and --y; a ringed lens outline is positioned at the same point and the native cursor is hidden.
Accessibility: A group labelled with the card title; the wireframe and lens are aria-hidden, so the finished content is what is read. Under prefers-reduced-motion the lens appears without easing.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "X-Ray" card.
Visual behavior: A finished card with an x-ray lens: move the pointer over it and a round lens follows, showing the wireframe underneath, the same layout drawn as dashed boxes on a grid with each element's spec.
Technique:
- Two faces share one grid layout: the finished card, and a wireframe copy on a 12px grid where each element is a dashed box labelled with its spec.
- The wireframe is clipped with clip-path: circle(r at var(--x) var(--y)); r is 0 at rest and 46px on hover, so the lens opens and closes smoothly.
- pointermove writes --x and --y; a ringed lens outline is positioned at the same point and the native cursor is hidden.
Accessibility: A group labelled with the card title; the wireframe and lens are aria-hidden, so the finished content is what is read. Under prefers-reduced-motion the lens appears without easing.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A finished card with an x-ray lens: move the pointer over it and a round lens follows, showing the wireframe underneath, the same layout drawn as dashed boxes on a grid with each element's spec.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, mouse tracking, spotlight, card
- Techniques
- clip path, css variables, mouse tracking
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A group labelled with the card title; the wireframe and lens are aria-hidden, so the finished content is what is read. Under prefers-reduced-motion the lens appears without easing.