20 / 21
All 21 in 21 Tailwind CSS Pricing Sections
- 01Tailwind Classic Three Tier Pricing Section
- 02Tailwind Highlighted Popular Pricing Tier
- 03Tailwind Feature Comparison Pricing Table
- 04Tailwind Monthly Yearly Billing Toggle Pricing
- 05Tailwind Usage Slider Pricing Estimator
- 06Tailwind Single Premium Pricing Card
- 07Tailwind Bento Pricing Grid
- 08Tailwind Vertical Tier Timeline Pricing
- 09Tailwind Brutalist Print Pricing Zine
- 10Tailwind Glass Aurora Pricing Cards
- 11Tailwind Per-Seat Pricing Calculator
- 12Tailwind Animated Conic Border Pricing Cards
- 13Tailwind Editorial Two Column Pricing List
- 14Tailwind 3D Tilt Perspective Pricing Cards
- 15Tailwind Synthwave Neon Pricing Section
- 16Tailwind Pay What You Want Pricing Card
- 17Tailwind Currency Switcher Pricing Section
- 18Tailwind Social Proof Marquee Pricing
- 19Tailwind Feature Accordion Pricing Tiers
- 20Tailwind Cursor Spotlight Pricing Cards
- 21Tailwind Annual Savings Stripe Pricing
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.
Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49
Cruip · Affiliate link
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-['>_'] before:text-tps20-amber">Self-hosted</li>
<li class="before:content-['>_'] before:text-tps20-amber">MIT licence</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Hosted & maintained</li>
<li class="before:content-['>_'] before:text-tps20-amber">Auto-updates + backups</li>
<li class="before:content-['>_'] before:text-tps20-amber">Email support</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Commercial licence</li>
<li class="before:content-['>_'] before:text-tps20-amber">Priority email support</li>
<li class="before:content-['>_'] 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><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-['>_'] before:text-tps20-amber">Self-hosted</li>
<li class="before:content-['>_'] before:text-tps20-amber">MIT licence</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Hosted & maintained</li>
<li class="before:content-['>_'] before:text-tps20-amber">Auto-updates + backups</li>
<li class="before:content-['>_'] before:text-tps20-amber">Email support</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Commercial licence</li>
<li class="before:content-['>_'] before:text-tps20-amber">Priority email support</li>
<li class="before:content-['>_'] 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;
}@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%');
});
});
})();(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.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@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;
}@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;
}<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">
<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:
setPropertytwice, and every visual decision — when the glow appears, how fast, what shape, what happens underprefers-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 }becomesgroup-hover:opacity-100 group-focus-within:opacity-100on the glow itself..ps-20__card > *:not(.ps-20__glow) { position: relative; z-index: 1 }becomesrelative z-1on 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 firstpointermove. --alpha(--color-tps20-amber/40)and--alpha(…/10)replace tworgba(245,158,11,…)literals, tying the light to the same token as the borders and bullets.- The
>prompt bullet is writtenbefore:content-['>_']— the underscore is Tailwind's space escape, and the glyph is HTML-escaped so the class list survives being pasted into markup.
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 — 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-['>_'] before:text-tps20-amber">Self-hosted</li>
<li class="before:content-['>_'] before:text-tps20-amber">MIT licence</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Hosted & maintained</li>
<li class="before:content-['>_'] before:text-tps20-amber">Auto-updates + backups</li>
<li class="before:content-['>_'] before:text-tps20-amber">Email support</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Commercial licence</li>
<li class="before:content-['>_'] before:text-tps20-amber">Priority email support</li>
<li class="before:content-['>_'] 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%');
});
});
})();
```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 — 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-['>_'] before:text-tps20-amber">Self-hosted</li>
<li class="before:content-['>_'] before:text-tps20-amber">MIT licence</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Hosted & maintained</li>
<li class="before:content-['>_'] before:text-tps20-amber">Auto-updates + backups</li>
<li class="before:content-['>_'] before:text-tps20-amber">Email support</li>
<li class="before:content-['>_'] 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-['>_'] before:text-tps20-amber">Commercial licence</li>
<li class="before:content-['>_'] before:text-tps20-amber">Priority email support</li>
<li class="before:content-['>_'] 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 circlein 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-amberand--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
isolateon 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
focushandler. Without it the variables are unset when a keyboard user arrives and the light appears in the top-left corner (the50%fallbacks only apply until the firstpointermovewrites 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-noneon 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.