Tailwind + JSMIT licensed

Tailwind Cursor Spotlight Pricing Cards

Amber light follows the cursor across black cards — and lands centred when you tab to one, so keyboard users get the same affordance. The JS sets two custom properties and stops there; group-hover: and group-focus-within: own the reveal, which is the cleanest possible division between script and style.

Live Demo
Plain CSSTailwind v4
Try it

Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49

Cruip · Affiliate link

Browse Cruip templates

The code

<section class="tps-20 w-full min-h-svh bg-tps20-bg font-tps20-mono text-tps20-body px-[clamp(20px,4vw,40px)] py-[clamp(28px,5vw,56px)]" aria-label="Pricing">
  <header class="text-center mb-[clamp(24px,4vw,40px)]">
    <h2 class="m-0 mb-2 font-tps20-display text-[clamp(22px,3.4vw,32px)] font-bold tracking-[-0.02em] text-white">Move the light. Read the price.</h2>
    <p class="m-0 text-[13.5px] text-tps20-muted">Cursor-aware cards. Real cards, real prices, real keyboard support.</p>
  </header>
  <div class="grid grid-cols-1 md:grid-cols-[repeat(3,minmax(0,1fr))] items-stretch gap-3.5 max-w-[360px] md:max-w-[980px] mx-auto">
    <article data-tps20-card tabindex="0" class="group relative isolate overflow-hidden flex flex-col px-[22px] pt-6 pb-5 rounded-[14px] bg-tps20-card border border-tps20-line transition-[border-color,translate] duration-200 hover:border-tps20-amber hover:-translate-y-0.5 focus-within:border-tps20-amber focus-within:-translate-y-0.5 focus:outline-0 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:focus-within:translate-y-0">
      <div class="absolute inset-0 z-0 pointer-events-none opacity-0 transition-opacity duration-[250ms] group-hover:opacity-100 group-focus-within:opacity-100 bg-[radial-gradient(180px_circle_at_var(--tps20-x,50%)_var(--tps20-y,50%),--alpha(--color-tps20-amber/40)_0%,--alpha(--color-tps20-amber/10)_30%,transparent_60%)] motion-reduce:transition-none" aria-hidden="true"></div>
      <h3 class="relative z-1 m-0 mb-3 text-[13px] font-bold uppercase tracking-[0.16em] text-tps20-amber">OSS</h3>
      <div class="relative z-1 flex items-baseline gap-2 mb-[18px] text-[38px] font-extrabold tracking-[-0.03em] text-white">$0<span class="text-xs font-medium tracking-[0.04em] text-tps20-muted">open source</span></div>
      <ul class="relative z-1 list-none p-0 m-0 mb-5 flex-1 text-[12.5px]/[2] text-tps20-list">
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Self-hosted</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">MIT licence</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">GitHub issues</li>
      </ul>
      <a href="#oss" class="relative z-1 block text-center px-4 py-2.5 rounded-[6px] bg-transparent text-tps20-amber border-[1.5px] border-tps20-amber-dark text-[11.5px] font-bold uppercase tracking-[0.1em] no-underline transition-[background-color,color,border-color] duration-[180ms] hover:bg-tps20-amber hover:text-tps20-bg hover:border-tps20-amber focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none">Clone the repo</a>
    </article>
    <article data-tps20-card tabindex="0" class="group relative isolate overflow-hidden flex flex-col px-[22px] pt-6 pb-5 rounded-[14px] bg-linear-180 from-tps20-card-hi to-tps20-card border border-tps20-amber-dark transition-[border-color,translate] duration-200 hover:border-tps20-amber hover:-translate-y-0.5 focus-within:border-tps20-amber focus-within:-translate-y-0.5 focus:outline-0 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:focus-within:translate-y-0">
      <div class="absolute inset-0 z-0 pointer-events-none opacity-0 transition-opacity duration-[250ms] group-hover:opacity-100 group-focus-within:opacity-100 bg-[radial-gradient(180px_circle_at_var(--tps20-x,50%)_var(--tps20-y,50%),--alpha(--color-tps20-amber/40)_0%,--alpha(--color-tps20-amber/10)_30%,transparent_60%)] motion-reduce:transition-none" aria-hidden="true"></div>
      <span class="relative z-1 self-start mb-2.5 px-2.5 py-0.75 rounded-[4px] bg-tps20-amber text-tps20-bg text-[10px] font-bold uppercase tracking-[0.12em]">Recommended</span>
      <h3 class="relative z-1 m-0 mb-3 text-[13px] font-bold uppercase tracking-[0.16em] text-tps20-amber">Cloud</h3>
      <div class="relative z-1 flex items-baseline gap-2 mb-[18px] text-[38px] font-extrabold tracking-[-0.03em] text-white">$24<span class="text-xs font-medium tracking-[0.04em] text-tps20-muted">/mo</span></div>
      <ul class="relative z-1 list-none p-0 m-0 mb-5 flex-1 text-[12.5px]/[2] text-tps20-list">
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Hosted &amp; maintained</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Auto-updates + backups</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Email support</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">API access</li>
      </ul>
      <a href="#cloud" class="relative z-1 block text-center px-4 py-2.5 rounded-[6px] bg-tps20-amber text-tps20-bg border-[1.5px] border-tps20-amber text-[11.5px] font-bold uppercase tracking-[0.1em] no-underline transition-[filter] duration-[180ms] hover:brightness-110 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none">Choose Cloud</a>
    </article>
    <article data-tps20-card tabindex="0" class="group relative isolate overflow-hidden flex flex-col px-[22px] pt-6 pb-5 rounded-[14px] bg-tps20-card border border-tps20-line transition-[border-color,translate] duration-200 hover:border-tps20-amber hover:-translate-y-0.5 focus-within:border-tps20-amber focus-within:-translate-y-0.5 focus:outline-0 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:focus-within:translate-y-0">
      <div class="absolute inset-0 z-0 pointer-events-none opacity-0 transition-opacity duration-[250ms] group-hover:opacity-100 group-focus-within:opacity-100 bg-[radial-gradient(180px_circle_at_var(--tps20-x,50%)_var(--tps20-y,50%),--alpha(--color-tps20-amber/40)_0%,--alpha(--color-tps20-amber/10)_30%,transparent_60%)] motion-reduce:transition-none" aria-hidden="true"></div>
      <h3 class="relative z-1 m-0 mb-3 text-[13px] font-bold uppercase tracking-[0.16em] text-tps20-amber">Self-Hosted Pro</h3>
      <div class="relative z-1 flex items-baseline gap-2 mb-[18px] text-[38px] font-extrabold tracking-[-0.03em] text-white">$199<span class="text-xs font-medium tracking-[0.04em] text-tps20-muted">/yr</span></div>
      <ul class="relative z-1 list-none p-0 m-0 mb-5 flex-1 text-[12.5px]/[2] text-tps20-list">
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Commercial licence</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Priority email support</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Audit logs</li>
      </ul>
      <a href="#shp" class="relative z-1 block text-center px-4 py-2.5 rounded-[6px] bg-transparent text-tps20-amber border-[1.5px] border-tps20-amber-dark text-[11.5px] font-bold uppercase tracking-[0.1em] no-underline transition-[background-color,color,border-color] duration-[180ms] hover:bg-tps20-amber hover:text-tps20-bg hover:border-tps20-amber focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none">Buy licence</a>
    </article>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tps20-bg: #050505;
  --color-tps20-bg: oklch(0.09 0 0);
  --color-tps20-card: #0a0a0a;
  --color-tps20-card: oklch(0.13 0 0);
  --color-tps20-card-hi: #1a0f02;
  --color-tps20-card-hi: oklch(0.18 0.03 70);
  --color-tps20-line: #2a2418;
  --color-tps20-line: oklch(0.26 0.02 80);
  --color-tps20-body: #f5e6c8;
  --color-tps20-body: oklch(0.93 0.04 85);
  --color-tps20-list: #d8c69a;
  --color-tps20-list: oklch(0.84 0.06 88);
  --color-tps20-muted: #a89470;
  --color-tps20-muted: oklch(0.68 0.05 85);
  --color-tps20-amber: #f59e0b;
  --color-tps20-amber: oklch(0.75 0.16 70);
  --color-tps20-amber-dark: #b45309;
  --color-tps20-amber-dark: oklch(0.55 0.15 55);
  --font-tps20-mono: 'JetBrains Mono', monospace;
  --font-tps20-display: 'Inter Tight', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the section is sized correctly in the moment before the
   browser build has compiled its utilities. */

.tps-20 {
  width: 100%;
  min-height: 100svh;
}
(function () {
  // The script's whole contribution is two custom properties. The reveal,
  // the fade, the gradient and the reduced-motion fallback are all utilities.
  var cards = document.querySelectorAll('[data-tps20-card]');
  Array.prototype.forEach.call(cards, function (card) {
    card.addEventListener('pointermove', function (e) {
      var rect = card.getBoundingClientRect();
      card.style.setProperty('--tps20-x', ((e.clientX - rect.left) / rect.width) * 100 + '%');
      card.style.setProperty('--tps20-y', ((e.clientY - rect.top) / rect.height) * 100 + '%');
    });
    // Keyboard parity: park the light in the middle when the card is focused.
    card.addEventListener('focus', function () {
      card.style.setProperty('--tps20-x', '50%');
      card.style.setProperty('--tps20-y', '50%');
    });
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tps20-bg: #050505;
  --color-tps20-bg: oklch(0.09 0 0);
  --color-tps20-card: #0a0a0a;
  --color-tps20-card: oklch(0.13 0 0);
  --color-tps20-card-hi: #1a0f02;
  --color-tps20-card-hi: oklch(0.18 0.03 70);
  --color-tps20-line: #2a2418;
  --color-tps20-line: oklch(0.26 0.02 80);
  --color-tps20-body: #f5e6c8;
  --color-tps20-body: oklch(0.93 0.04 85);
  --color-tps20-list: #d8c69a;
  --color-tps20-list: oklch(0.84 0.06 88);
  --color-tps20-muted: #a89470;
  --color-tps20-muted: oklch(0.68 0.05 85);
  --color-tps20-amber: #f59e0b;
  --color-tps20-amber: oklch(0.75 0.16 70);
  --color-tps20-amber-dark: #b45309;
  --color-tps20-amber-dark: oklch(0.55 0.15 55);
  --font-tps20-mono: 'JetBrains Mono', monospace;
  --font-tps20-display: 'Inter Tight', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the section is sized correctly in the moment before the
   browser build has compiled its utilities. */

.tps-20 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter+Tight:wght@700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The division of labour is the point: setProperty twice, and every visual decision — when the glow appears, how fast, what shape, what happens under prefers-reduced-motion — lives in the class list. The CSS version splits the same behaviour across four rules and a selector that has to know the glow's class name.
  • .ps-20__card:hover .ps-20__glow, .ps-20__card:focus-within .ps-20__glow { opacity: 1 } becomes group-hover:opacity-100 group-focus-within:opacity-100 on the glow itself.
  • .ps-20__card > *:not(.ps-20__glow) { position: relative; z-index: 1 } becomes relative z-1 on each child. More characters, but nothing breaks when a new child is added that the :not() selector never heard of.
  • The pointer variables keep their 50% fallbacks inside the arbitrary gradient value — var(--tps20-x,50%) — so the glow is centred before the first pointermove.
  • --alpha(--color-tps20-amber/40) and --alpha(…/10) replace two rgba(245,158,11,…) literals, tying the light to the same token as the borders and bullets.
  • The > prompt bullet is written before:content-['>_'] — the underscore is Tailwind's space escape, and the glyph is HTML-escaped so the class list survives being pasted into markup.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Cursor Spotlight Pricing Cards
Source: https://codefronts.com/layouts/tailwind-css-pricing-sections/tailwind-cursor-spotlight-pricing-cards/

Amber light follows the cursor across black cards &mdash; and lands centred when you tab to one, so keyboard users get the same affordance. The JS sets two custom properties and stops there; group-hover: and group-focus-within: own the reveal, which is the cleanest possible division between script and style.
## HTML
```html
<section class="tps-20 w-full min-h-svh bg-tps20-bg font-tps20-mono text-tps20-body px-[clamp(20px,4vw,40px)] py-[clamp(28px,5vw,56px)]" aria-label="Pricing">
  <header class="text-center mb-[clamp(24px,4vw,40px)]">
    <h2 class="m-0 mb-2 font-tps20-display text-[clamp(22px,3.4vw,32px)] font-bold tracking-[-0.02em] text-white">Move the light. Read the price.</h2>
    <p class="m-0 text-[13.5px] text-tps20-muted">Cursor-aware cards. Real cards, real prices, real keyboard support.</p>
  </header>
  <div class="grid grid-cols-1 md:grid-cols-[repeat(3,minmax(0,1fr))] items-stretch gap-3.5 max-w-[360px] md:max-w-[980px] mx-auto">
    <article data-tps20-card tabindex="0" class="group relative isolate overflow-hidden flex flex-col px-[22px] pt-6 pb-5 rounded-[14px] bg-tps20-card border border-tps20-line transition-[border-color,translate] duration-200 hover:border-tps20-amber hover:-translate-y-0.5 focus-within:border-tps20-amber focus-within:-translate-y-0.5 focus:outline-0 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:focus-within:translate-y-0">
      <div class="absolute inset-0 z-0 pointer-events-none opacity-0 transition-opacity duration-[250ms] group-hover:opacity-100 group-focus-within:opacity-100 bg-[radial-gradient(180px_circle_at_var(--tps20-x,50%)_var(--tps20-y,50%),--alpha(--color-tps20-amber/40)_0%,--alpha(--color-tps20-amber/10)_30%,transparent_60%)] motion-reduce:transition-none" aria-hidden="true"></div>
      <h3 class="relative z-1 m-0 mb-3 text-[13px] font-bold uppercase tracking-[0.16em] text-tps20-amber">OSS</h3>
      <div class="relative z-1 flex items-baseline gap-2 mb-[18px] text-[38px] font-extrabold tracking-[-0.03em] text-white">$0<span class="text-xs font-medium tracking-[0.04em] text-tps20-muted">open source</span></div>
      <ul class="relative z-1 list-none p-0 m-0 mb-5 flex-1 text-[12.5px]/[2] text-tps20-list">
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Self-hosted</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">MIT licence</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">GitHub issues</li>
      </ul>
      <a href="#oss" class="relative z-1 block text-center px-4 py-2.5 rounded-[6px] bg-transparent text-tps20-amber border-[1.5px] border-tps20-amber-dark text-[11.5px] font-bold uppercase tracking-[0.1em] no-underline transition-[background-color,color,border-color] duration-[180ms] hover:bg-tps20-amber hover:text-tps20-bg hover:border-tps20-amber focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none">Clone the repo</a>
    </article>
    <article data-tps20-card tabindex="0" class="group relative isolate overflow-hidden flex flex-col px-[22px] pt-6 pb-5 rounded-[14px] bg-linear-180 from-tps20-card-hi to-tps20-card border border-tps20-amber-dark transition-[border-color,translate] duration-200 hover:border-tps20-amber hover:-translate-y-0.5 focus-within:border-tps20-amber focus-within:-translate-y-0.5 focus:outline-0 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:focus-within:translate-y-0">
      <div class="absolute inset-0 z-0 pointer-events-none opacity-0 transition-opacity duration-[250ms] group-hover:opacity-100 group-focus-within:opacity-100 bg-[radial-gradient(180px_circle_at_var(--tps20-x,50%)_var(--tps20-y,50%),--alpha(--color-tps20-amber/40)_0%,--alpha(--color-tps20-amber/10)_30%,transparent_60%)] motion-reduce:transition-none" aria-hidden="true"></div>
      <span class="relative z-1 self-start mb-2.5 px-2.5 py-0.75 rounded-[4px] bg-tps20-amber text-tps20-bg text-[10px] font-bold uppercase tracking-[0.12em]">Recommended</span>
      <h3 class="relative z-1 m-0 mb-3 text-[13px] font-bold uppercase tracking-[0.16em] text-tps20-amber">Cloud</h3>
      <div class="relative z-1 flex items-baseline gap-2 mb-[18px] text-[38px] font-extrabold tracking-[-0.03em] text-white">$24<span class="text-xs font-medium tracking-[0.04em] text-tps20-muted">/mo</span></div>
      <ul class="relative z-1 list-none p-0 m-0 mb-5 flex-1 text-[12.5px]/[2] text-tps20-list">
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Hosted &amp; maintained</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Auto-updates + backups</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Email support</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">API access</li>
      </ul>
      <a href="#cloud" class="relative z-1 block text-center px-4 py-2.5 rounded-[6px] bg-tps20-amber text-tps20-bg border-[1.5px] border-tps20-amber text-[11.5px] font-bold uppercase tracking-[0.1em] no-underline transition-[filter] duration-[180ms] hover:brightness-110 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none">Choose Cloud</a>
    </article>
    <article data-tps20-card tabindex="0" class="group relative isolate overflow-hidden flex flex-col px-[22px] pt-6 pb-5 rounded-[14px] bg-tps20-card border border-tps20-line transition-[border-color,translate] duration-200 hover:border-tps20-amber hover:-translate-y-0.5 focus-within:border-tps20-amber focus-within:-translate-y-0.5 focus:outline-0 focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:focus-within:translate-y-0">
      <div class="absolute inset-0 z-0 pointer-events-none opacity-0 transition-opacity duration-[250ms] group-hover:opacity-100 group-focus-within:opacity-100 bg-[radial-gradient(180px_circle_at_var(--tps20-x,50%)_var(--tps20-y,50%),--alpha(--color-tps20-amber/40)_0%,--alpha(--color-tps20-amber/10)_30%,transparent_60%)] motion-reduce:transition-none" aria-hidden="true"></div>
      <h3 class="relative z-1 m-0 mb-3 text-[13px] font-bold uppercase tracking-[0.16em] text-tps20-amber">Self-Hosted Pro</h3>
      <div class="relative z-1 flex items-baseline gap-2 mb-[18px] text-[38px] font-extrabold tracking-[-0.03em] text-white">$199<span class="text-xs font-medium tracking-[0.04em] text-tps20-muted">/yr</span></div>
      <ul class="relative z-1 list-none p-0 m-0 mb-5 flex-1 text-[12.5px]/[2] text-tps20-list">
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Commercial licence</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Priority email support</li>
        <li class="before:content-['&gt;_'] before:text-tps20-amber">Audit logs</li>
      </ul>
      <a href="#shp" class="relative z-1 block text-center px-4 py-2.5 rounded-[6px] bg-transparent text-tps20-amber border-[1.5px] border-tps20-amber-dark text-[11.5px] font-bold uppercase tracking-[0.1em] no-underline transition-[background-color,color,border-color] duration-[180ms] hover:bg-tps20-amber hover:text-tps20-bg hover:border-tps20-amber focus-visible:outline-2 focus-visible:outline-tps20-amber focus-visible:outline-offset-[3px] motion-reduce:transition-none">Buy licence</a>
    </article>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tps20-bg: #050505;
  --color-tps20-bg: oklch(0.09 0 0);
  --color-tps20-card: #0a0a0a;
  --color-tps20-card: oklch(0.13 0 0);
  --color-tps20-card-hi: #1a0f02;
  --color-tps20-card-hi: oklch(0.18 0.03 70);
  --color-tps20-line: #2a2418;
  --color-tps20-line: oklch(0.26 0.02 80);
  --color-tps20-body: #f5e6c8;
  --color-tps20-body: oklch(0.93 0.04 85);
  --color-tps20-list: #d8c69a;
  --color-tps20-list: oklch(0.84 0.06 88);
  --color-tps20-muted: #a89470;
  --color-tps20-muted: oklch(0.68 0.05 85);
  --color-tps20-amber: #f59e0b;
  --color-tps20-amber: oklch(0.75 0.16 70);
  --color-tps20-amber-dark: #b45309;
  --color-tps20-amber-dark: oklch(0.55 0.15 55);
  --font-tps20-mono: 'JetBrains Mono', monospace;
  --font-tps20-display: 'Inter Tight', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the section is sized correctly in the moment before the
   browser build has compiled its utilities. */

.tps-20 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(function () {
  // The script's whole contribution is two custom properties. The reveal,
  // the fade, the gradient and the reduced-motion fallback are all utilities.
  var cards = document.querySelectorAll('[data-tps20-card]');
  Array.prototype.forEach.call(cards, function (card) {
    card.addEventListener('pointermove', function (e) {
      var rect = card.getBoundingClientRect();
      card.style.setProperty('--tps20-x', ((e.clientX - rect.left) / rect.width) * 100 + '%');
      card.style.setProperty('--tps20-y', ((e.clientY - rect.top) / rect.height) * 100 + '%');
    });
    // Keyboard parity: park the light in the middle when the card is focused.
    card.addEventListener('focus', function () {
      card.style.setProperty('--tps20-x', '50%');
      card.style.setProperty('--tps20-y', '50%');
    });
  });
})();
```

How this works

The script's entire job is two numbers. A pointermove handler converts the cursor to a percentage of the card and writes it:

card.style.setProperty('--tps20-x', x + '%');
card.style.setProperty('--tps20-y', y + '%');

No classes are added, no styles are read, nothing is measured on every frame except the card's own rect. The gradient, the fade, the two reveal triggers and the reduced-motion fallback are all utilities.

group-focus-within: is what makes this accessible. A spotlight tied only to :hover is invisible to keyboard users. Here the cards are focusable, the focus handler parks the light at 50%/50%, and group-focus-within:opacity-100 reveals it — so tabbing through the tiers lights each one in turn.

isolate is load-bearing. Without a stacking context on the card, the absolutely-positioned glow at z-0 composites against the section rather than the card, and the effect either vanishes behind the card's background or bleeds over its neighbours.

Contrast does not depend on the light. Every card reads correctly with the glow off — amber on near-black for headings, warm parchment for body copy. The spotlight is decoration, and it is aria-hidden because that is what it is.

Make it yours

  • Spotlight size: the 180px circle in the gradient. Larger reads as ambient light, smaller as a torch.
  • Falloff: the three gradient stops (40% / 10% / transparent at 60%). Flatten them for a softer wash.
  • Accent: --color-tps20-amber and --color-tps20-amber-dark — the light, the borders, the bullets and the CTAs all derive from the pair.
  • Fade speed: duration-[250ms] on the glow. Faster feels twitchy as the pointer crosses cards.
  • Prompt character: the > bullet and the mono CTA labels are the developer-tool voice. Swap them for ticks and sentence case and the same layout reads as a normal SaaS page.

Gotchas — read before shipping

  • isolate on the card is required. Remove it and the glow's stacking context is the section, which either hides the light or lets it spill across cards.
  • Every content child needs relative z-1. The CSS does it with one > *:not(.glow) rule; in utilities it is per-element, and a forgotten child sits under the light.
  • The glow must be pointer-events-none, or it intercepts the clicks meant for the CTA beneath it.
  • Keep the focus handler. Without it the variables are unset when a keyboard user arrives and the light appears in the top-left corner (the 50% fallbacks only apply until the first pointermove writes real values).
  • tabindex="0" on a non-interactive <article> is a deliberate trade: it buys keyboard parity for the effect at the cost of an extra tab stop per card. If that is unacceptable, drop both the tabindex and the focus styling — do not keep one without the other.
  • motion-reduce:transition-none on the glow and the cards. The light still follows the pointer, which is direct manipulation rather than animation, but nothing fades or lifts on its own.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Custom properties in gradient positions, isolation and :focus-within are all Baseline well before 2020. pointermove covers mouse, pen and touch with one handler.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

HTML & JSno Tailwind involved

Search CodeFronts

Loading…