Plate 03
Tilt Card
Leans toward the cursor, settles flat when it leaves.
Experiment #005
On mousemove, Alpine writes the pointer position into two CSS custom properties (--x, --y) on the card — plain style writes, naturally throttled by the render loop, no rAF needed. Two overlay layers consume them: a radial-gradient halo and a 1px border ring isolated with a two-layer mask-composite, both built on color-mix with currentColor so they adapt to light and dark themes. Only opacity is ever transitioned; the gradient position snaps to the cursor.
Move your cursor across the card. A soft halo tracks it, and the border catches the light as it passes.
Lab notes
Tune the halo with the gradient radius (180–280px) and the color-mix percentage (6–12%) — past that it stops reading as light and starts reading as a stain. The border ring uses mask-composite: exclude, supported in all modern browsers. If the card wraps a link, mirror the hover state with :focus-within and center --x/--y so keyboard users get the highlight too.
<!-- Spotlight Card — Tailwind + Alpine.js -->
<div
x-data="{
active: false,
move(event) {
const rect = this.$el.getBoundingClientRect()
this.$el.style.setProperty('--x', `${event.clientX - rect.left}px`)
this.$el.style.setProperty('--y', `${event.clientY - rect.top}px`)
},
}"
@mousemove="move"
@mouseenter="active = true"
@mouseleave="active = false"
style="--x: 50%; --y: 40%"
class="relative w-72 border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900"
>
<!-- Halo: radial gradient centered on the cursor via --x/--y -->
<div
aria-hidden="true"
:class="active ? 'opacity-100' : 'opacity-0'"
class="pointer-events-none absolute inset-0 text-zinc-900 opacity-0
transition-opacity duration-300 motion-reduce:transition-none
dark:text-white"
style="background: radial-gradient(220px circle at var(--x) var(--y), color-mix(in oklab, currentColor 8%, transparent), transparent 70%)"
></div>
<!-- Border ring: same gradient, masked down to a 1px frame -->
<div
aria-hidden="true"
:class="active ? 'opacity-100' : 'opacity-0'"
class="pointer-events-none absolute inset-0 p-px text-zinc-900 opacity-0
transition-opacity duration-300 motion-reduce:transition-none
dark:text-white"
style="background: radial-gradient(140px circle at var(--x) var(--y), color-mix(in oklab, currentColor 40%, transparent), transparent 70%); mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude"
></div>
<!-- Card content -->
<div class="relative">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-white">Spotlight</h3>
<p class="mt-2 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">
Move your cursor across the card. A soft halo tracks it,
and the border catches the light as it passes.
</p>
</div>
</div>
// Spotlight Card — reusable Alpine.js component
//
// Register once at boot, then apply to any card:
//
// <div
// x-data="spotlightCard"
// @mousemove="move"
// @mouseenter="active = true"
// @mouseleave="active = false"
// class="relative ..."
// >
// <div aria-hidden="true" :class="active ? 'opacity-100' : 'opacity-0'" ...></div>
// ...
// </div>
//
// The component writes the pointer position into --x/--y on the root
// element. Overlay layers position their radial-gradient with
// `at var(--x) var(--y)` and toggle visibility on `active`.
document.addEventListener('alpine:init', () => {
Alpine.data('spotlightCard', () => ({
active: false,
init() {
// Sensible resting point before the first mousemove.
this.$el.style.setProperty('--x', '50%')
this.$el.style.setProperty('--y', '40%')
},
move(event) {
const rect = this.$el.getBoundingClientRect()
this.$el.style.setProperty('--x', `${event.clientX - rect.left}px`)
this.$el.style.setProperty('--y', `${event.clientY - rect.top}px`)
},
}))
})
Adjacent
Plate 03
Leans toward the cursor, settles flat when it leaves.