05 / 20
All 20 in 20 CSS Neon Effects
- 01CSS Glow Anatomy with Layered Box Shadow
- 02Drop Shadow Glow for Irregular Shapes
- 03Neon Color Choice and Perceived Luminance
- 04Inner Glow and Faked Light Emission
- 05Animating Glow Intensity with at-property
- 06Glow on SVG Paths and Stroke Elements
- 07Neon Glow Paint Cost and What to Animate
- 08Accessible Neon on Dark Backgrounds
- 09Neon Glow Button Construction Reference
- 10Neon Heading and Display Type Glow
- 11Neon Pricing Table with Glow Tiers
- 12Layered Glow Scene with Depth Falloff
- 13Neon Form Inputs and Focus Glow
- 14Neon Navigation with Active Glow State
- 15Flicker Timing and Failing Tube Effect
- 16Cyberpunk Neon Palette and Token Set
- 17Tailwind Neon Glow Utility Recipe
- 18Neon Icon Glow with SVG Filters
- 19Infinite Neon Grid and Perspective Floor
- 20Multi Hue Neon Gradient Glow
Animating Glow Intensity with at-property
An unregistered custom property cannot interpolate, so animating --glow steps between values instead of easing between them — the single most common "why won't my neon pulse" question. Registering it with @property and a typed syntax makes intensity animatable independently of hue. The broken version sits beside the working one.
Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time
Untitled UI · Affiliate link
The code
<section class="nn-05" aria-labelledby="nn-05-heading">
<div class="nn-05__stage">
<div class="nn-05__theme">
<input class="nn-05__tin" type="checkbox" id="nn-05-theme">
<label class="nn-05__tlab" for="nn-05-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-05__head">
<p class="nn-05__kicker">Typed tokens</p>
<h2 class="nn-05__heading" id="nn-05-heading">A custom property has to be typed before it can breathe</h2>
<p class="nn-05__sub">Both lamps run the same keyframes on the same token name. Only one of them registered it.</p>
</header>
<div class="nn-05__compare">
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--bad">Unregistered</span>
Steps at the midpoint — reads as nothing happening
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--raw">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">--nn-05-raw: .35 → 1 / no @property</p>
</figure>
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--good">Registered</span>
Interpolates — a real breath, hue unchanged
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--reg">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">@property --nn-05-i { syntax: '<number>' }</p>
</figure>
</div>
<div class="nn-05__code">
<p class="nn-05__clabel">The registration</p>
<pre class="nn-05__pre"><code>@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}</code></pre>
<p class="nn-05__cnote">The token drives <strong>opacity on a glow layer that is built once</strong> — not the blur radius, and not the shadow colour. Intensity animates; the expensive part never repaints.</p>
</div>
</div>
</section><section class="nn-05" aria-labelledby="nn-05-heading">
<div class="nn-05__stage">
<div class="nn-05__theme">
<input class="nn-05__tin" type="checkbox" id="nn-05-theme">
<label class="nn-05__tlab" for="nn-05-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-05__head">
<p class="nn-05__kicker">Typed tokens</p>
<h2 class="nn-05__heading" id="nn-05-heading">A custom property has to be typed before it can breathe</h2>
<p class="nn-05__sub">Both lamps run the same keyframes on the same token name. Only one of them registered it.</p>
</header>
<div class="nn-05__compare">
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--bad">Unregistered</span>
Steps at the midpoint — reads as nothing happening
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--raw">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">--nn-05-raw: .35 → 1 / no @property</p>
</figure>
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--good">Registered</span>
Interpolates — a real breath, hue unchanged
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--reg">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">@property --nn-05-i { syntax: '<number>' }</p>
</figure>
</div>
<div class="nn-05__code">
<p class="nn-05__clabel">The registration</p>
<pre class="nn-05__pre"><code>@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}</code></pre>
<p class="nn-05__cnote">The token drives <strong>opacity on a glow layer that is built once</strong> — not the blur radius, and not the shadow colour. Intensity animates; the expensive part never repaints.</p>
</div>
</div>
</section>@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}
.nn-05 {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-spec-neon: #c2f24a;
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05 {
--nn-05-spec-ink: #f4ecf7;
--nn-05-spec-dim: #a894b2;
}
.nn-05__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-05__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-05__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-05__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-05__tlab:hover {
opacity: 1;
}
.nn-05__tin:focus-visible + .nn-05__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05 {
--nn-05-hue: 122;
--nn-05-dur: 3.6s;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
--nn-05-core: rgba(194, 242, 74, .9);
--nn-05-halo: rgba(194, 242, 74, .48);
--nn-05-bloom: rgba(194, 242, 74, .26);
--nn-05-raw: 1;
--nn-05-font-display: system-ui, "Helvetica Neue", Arial, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-05-bg);
color: var(--nn-05-ink);
font-family: system-ui, sans-serif;
}
.nn-05 *,
.nn-05 *::before,
.nn-05 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-05 {
--nn-05-bg: oklch(17% 0.035 310);
--nn-05-well: oklch(21% 0.045 310);
--nn-05-line: oklch(31% 0.055 310);
--nn-05-ink: oklch(96% 0.014 310);
--nn-05-muted: oklch(74% 0.03 310);
--nn-05-neon: oklch(90% 0.2 var(--nn-05-hue));
--nn-05-core: oklch(92% 0.2 var(--nn-05-hue) / .9);
--nn-05-halo: oklch(88% 0.19 var(--nn-05-hue) / .48);
--nn-05-bloom: oklch(84% 0.18 var(--nn-05-hue) / .26);
}
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
.nn-05[data-theme="dark"] {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
.nn-05__stage {
max-width: 66rem;
margin: 0 auto;
padding: clamp(1.5rem, 5vw, 2.25rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2rem, 6vw, 2.75rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-05__kicker {
margin: 0 0 .5rem;
font-size: .74rem;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--nn-05-neon);
}
.nn-05__heading {
margin: 0;
font-family: var(--nn-05-font-display);
font-size: clamp(1.4rem, 4.8vw, 2.1rem);
font-weight: 650;
letter-spacing: -.025em;
text-wrap: balance;
}
.nn-05__sub {
margin: .6rem 0 0;
max-width: 40rem;
font-size: .92rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__compare {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 52rem) {
.nn-05__compare {
grid-template-columns: 1fr 1fr;
}
}
.nn-05__cell {
margin: 0;
display: grid;
gap: .7rem;
min-inline-size: 0;
}
.nn-05__caption {
display: flex;
align-items: center;
gap: .6rem;
flex-wrap: wrap;
font-size: .78rem;
color: var(--nn-05-muted);
min-inline-size: 0;
}
.nn-05__tag {
padding: .22rem .5rem;
border-radius: .35rem;
border: 1px solid var(--nn-05-line);
font-size: .64rem;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
}
.nn-05__tag--bad {
color: var(--nn-05-bad);
border-color: #4b2833;
}
.nn-05__tag--good {
color: var(--nn-05-neon);
border-color: color-mix(in srgb, var(--nn-05-neon) 40%, transparent);
}
.nn-05__well {
display: grid;
place-items: center;
min-height: clamp(10rem, 28vw, 13rem);
padding: clamp(1.5rem, 6vw, 2.75rem);
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
background: var(--nn-05-well);
overflow: hidden;
}
.nn-05__lamp {
position: relative;
padding: 1rem 1.4rem;
border-radius: .6rem;
border: 1px solid var(--nn-05-spec-neon);
background: #191125;
isolation: isolate;
}
.nn-05__lamp::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
box-shadow: 0 0 .35rem var(--nn-05-core), 0 0 clamp(.8rem, 3.5vw, 1.25rem) var(--nn-05-halo), 0 0 clamp(1.75rem, 8vw, 3rem) .2rem var(--nn-05-bloom);
z-index: -1;
}
.nn-05__lamp--raw::after {
opacity: var(--nn-05-raw);
animation: nn-05-raw var(--nn-05-dur) ease-in-out infinite;
}
.nn-05__lamp--reg::after {
opacity: var(--nn-05-i);
animation: nn-05-reg var(--nn-05-dur) ease-in-out infinite;
}
@keyframes nn-05-raw {
0%,
100% {
--nn-05-raw: .35;
}
50% {
--nn-05-raw: 1;
}
}
@keyframes nn-05-reg {
0%,
100% {
--nn-05-i: .35;
}
50% {
--nn-05-i: 1;
}
}
.nn-05__label {
font-family: ui-monospace, Menlo, monospace;
font-size: clamp(.8rem, 2.6vw, .92rem);
color: var(--nn-05-spec-ink);
letter-spacing: .02em;
}
.nn-05__mono {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .68rem;
line-height: 1.5;
color: var(--nn-05-muted);
overflow-wrap: anywhere;
}
.nn-05__code {
display: grid;
gap: .6rem;
padding: 1.1rem 1.25rem;
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
}
.nn-05__clabel {
margin: 0;
font-size: .7rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-05-muted);
}
.nn-05__pre {
margin: 0;
overflow-x: auto;
font-family: ui-monospace, Menlo, monospace;
font-size: .74rem;
line-height: 1.7;
color: var(--nn-05-neon);
}
.nn-05__cnote {
margin: 0;
font-size: .84rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__cnote strong {
color: var(--nn-05-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-05__lamp--raw::after,
.nn-05__lamp--reg::after {
animation: none;
opacity: 1;
}
}@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}
.nn-05 {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-spec-neon: #c2f24a;
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05 {
--nn-05-spec-ink: #f4ecf7;
--nn-05-spec-dim: #a894b2;
}
.nn-05__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-05__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-05__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-05__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-05__tlab:hover {
opacity: 1;
}
.nn-05__tin:focus-visible + .nn-05__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05 {
--nn-05-hue: 122;
--nn-05-dur: 3.6s;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
--nn-05-core: rgba(194, 242, 74, .9);
--nn-05-halo: rgba(194, 242, 74, .48);
--nn-05-bloom: rgba(194, 242, 74, .26);
--nn-05-raw: 1;
--nn-05-font-display: system-ui, "Helvetica Neue", Arial, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-05-bg);
color: var(--nn-05-ink);
font-family: system-ui, sans-serif;
}
.nn-05 *,
.nn-05 *::before,
.nn-05 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-05 {
--nn-05-bg: oklch(17% 0.035 310);
--nn-05-well: oklch(21% 0.045 310);
--nn-05-line: oklch(31% 0.055 310);
--nn-05-ink: oklch(96% 0.014 310);
--nn-05-muted: oklch(74% 0.03 310);
--nn-05-neon: oklch(90% 0.2 var(--nn-05-hue));
--nn-05-core: oklch(92% 0.2 var(--nn-05-hue) / .9);
--nn-05-halo: oklch(88% 0.19 var(--nn-05-hue) / .48);
--nn-05-bloom: oklch(84% 0.18 var(--nn-05-hue) / .26);
}
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
.nn-05[data-theme="dark"] {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
.nn-05__stage {
max-width: 66rem;
margin: 0 auto;
padding: clamp(1.5rem, 5vw, 2.25rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2rem, 6vw, 2.75rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-05__kicker {
margin: 0 0 .5rem;
font-size: .74rem;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--nn-05-neon);
}
.nn-05__heading {
margin: 0;
font-family: var(--nn-05-font-display);
font-size: clamp(1.4rem, 4.8vw, 2.1rem);
font-weight: 650;
letter-spacing: -.025em;
text-wrap: balance;
}
.nn-05__sub {
margin: .6rem 0 0;
max-width: 40rem;
font-size: .92rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__compare {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 52rem) {
.nn-05__compare {
grid-template-columns: 1fr 1fr;
}
}
.nn-05__cell {
margin: 0;
display: grid;
gap: .7rem;
min-inline-size: 0;
}
.nn-05__caption {
display: flex;
align-items: center;
gap: .6rem;
flex-wrap: wrap;
font-size: .78rem;
color: var(--nn-05-muted);
min-inline-size: 0;
}
.nn-05__tag {
padding: .22rem .5rem;
border-radius: .35rem;
border: 1px solid var(--nn-05-line);
font-size: .64rem;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
}
.nn-05__tag--bad {
color: var(--nn-05-bad);
border-color: #4b2833;
}
.nn-05__tag--good {
color: var(--nn-05-neon);
border-color: color-mix(in srgb, var(--nn-05-neon) 40%, transparent);
}
.nn-05__well {
display: grid;
place-items: center;
min-height: clamp(10rem, 28vw, 13rem);
padding: clamp(1.5rem, 6vw, 2.75rem);
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
background: var(--nn-05-well);
overflow: hidden;
}
.nn-05__lamp {
position: relative;
padding: 1rem 1.4rem;
border-radius: .6rem;
border: 1px solid var(--nn-05-spec-neon);
background: #191125;
isolation: isolate;
}
.nn-05__lamp::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
box-shadow: 0 0 .35rem var(--nn-05-core), 0 0 clamp(.8rem, 3.5vw, 1.25rem) var(--nn-05-halo), 0 0 clamp(1.75rem, 8vw, 3rem) .2rem var(--nn-05-bloom);
z-index: -1;
}
.nn-05__lamp--raw::after {
opacity: var(--nn-05-raw);
animation: nn-05-raw var(--nn-05-dur) ease-in-out infinite;
}
.nn-05__lamp--reg::after {
opacity: var(--nn-05-i);
animation: nn-05-reg var(--nn-05-dur) ease-in-out infinite;
}
@keyframes nn-05-raw {
0%,
100% {
--nn-05-raw: .35;
}
50% {
--nn-05-raw: 1;
}
}
@keyframes nn-05-reg {
0%,
100% {
--nn-05-i: .35;
}
50% {
--nn-05-i: 1;
}
}
.nn-05__label {
font-family: ui-monospace, Menlo, monospace;
font-size: clamp(.8rem, 2.6vw, .92rem);
color: var(--nn-05-spec-ink);
letter-spacing: .02em;
}
.nn-05__mono {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .68rem;
line-height: 1.5;
color: var(--nn-05-muted);
overflow-wrap: anywhere;
}
.nn-05__code {
display: grid;
gap: .6rem;
padding: 1.1rem 1.25rem;
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
}
.nn-05__clabel {
margin: 0;
font-size: .7rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-05-muted);
}
.nn-05__pre {
margin: 0;
overflow-x: auto;
font-family: ui-monospace, Menlo, monospace;
font-size: .74rem;
line-height: 1.7;
color: var(--nn-05-neon);
}
.nn-05__cnote {
margin: 0;
font-size: .84rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__cnote strong {
color: var(--nn-05-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-05__lamp--raw::after,
.nn-05__lamp--reg::after {
animation: none;
opacity: 1;
}
}Here's a working CSS Neon Effect 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: Animating Glow Intensity with at-property
Source: https://codefronts.com/design-styles/css-neon/animating-glow-intensity-with-at-property/
An unregistered custom property cannot interpolate, so animating --glow steps between values instead of easing between them — the single most common "why won't my neon pulse" question. Registering it with @property and a typed syntax makes intensity animatable independently of hue. The broken version sits beside the working one.
## HTML
```html
<section class="nn-05" aria-labelledby="nn-05-heading">
<div class="nn-05__stage">
<div class="nn-05__theme">
<input class="nn-05__tin" type="checkbox" id="nn-05-theme">
<label class="nn-05__tlab" for="nn-05-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-05__head">
<p class="nn-05__kicker">Typed tokens</p>
<h2 class="nn-05__heading" id="nn-05-heading">A custom property has to be typed before it can breathe</h2>
<p class="nn-05__sub">Both lamps run the same keyframes on the same token name. Only one of them registered it.</p>
</header>
<div class="nn-05__compare">
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--bad">Unregistered</span>
Steps at the midpoint — reads as nothing happening
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--raw">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">--nn-05-raw: .35 → 1 / no @property</p>
</figure>
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--good">Registered</span>
Interpolates — a real breath, hue unchanged
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--reg">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">@property --nn-05-i { syntax: '<number>' }</p>
</figure>
</div>
<div class="nn-05__code">
<p class="nn-05__clabel">The registration</p>
<pre class="nn-05__pre"><code>@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}</code></pre>
<p class="nn-05__cnote">The token drives <strong>opacity on a glow layer that is built once</strong> — not the blur radius, and not the shadow colour. Intensity animates; the expensive part never repaints.</p>
</div>
</div>
</section>
```
## CSS
```css
@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}
.nn-05 {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-spec-neon: #c2f24a;
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05 {
--nn-05-spec-ink: #f4ecf7;
--nn-05-spec-dim: #a894b2;
}
.nn-05__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-05__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-05__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-05__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-05__tlab:hover {
opacity: 1;
}
.nn-05__tin:focus-visible + .nn-05__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05 {
--nn-05-hue: 122;
--nn-05-dur: 3.6s;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
--nn-05-core: rgba(194, 242, 74, .9);
--nn-05-halo: rgba(194, 242, 74, .48);
--nn-05-bloom: rgba(194, 242, 74, .26);
--nn-05-raw: 1;
--nn-05-font-display: system-ui, "Helvetica Neue", Arial, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-05-bg);
color: var(--nn-05-ink);
font-family: system-ui, sans-serif;
}
.nn-05 *,
.nn-05 *::before,
.nn-05 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-05 {
--nn-05-bg: oklch(17% 0.035 310);
--nn-05-well: oklch(21% 0.045 310);
--nn-05-line: oklch(31% 0.055 310);
--nn-05-ink: oklch(96% 0.014 310);
--nn-05-muted: oklch(74% 0.03 310);
--nn-05-neon: oklch(90% 0.2 var(--nn-05-hue));
--nn-05-core: oklch(92% 0.2 var(--nn-05-hue) / .9);
--nn-05-halo: oklch(88% 0.19 var(--nn-05-hue) / .48);
--nn-05-bloom: oklch(84% 0.18 var(--nn-05-hue) / .26);
}
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
.nn-05[data-theme="dark"] {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
.nn-05__stage {
max-width: 66rem;
margin: 0 auto;
padding: clamp(1.5rem, 5vw, 2.25rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2rem, 6vw, 2.75rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-05__kicker {
margin: 0 0 .5rem;
font-size: .74rem;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--nn-05-neon);
}
.nn-05__heading {
margin: 0;
font-family: var(--nn-05-font-display);
font-size: clamp(1.4rem, 4.8vw, 2.1rem);
font-weight: 650;
letter-spacing: -.025em;
text-wrap: balance;
}
.nn-05__sub {
margin: .6rem 0 0;
max-width: 40rem;
font-size: .92rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__compare {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 52rem) {
.nn-05__compare {
grid-template-columns: 1fr 1fr;
}
}
.nn-05__cell {
margin: 0;
display: grid;
gap: .7rem;
min-inline-size: 0;
}
.nn-05__caption {
display: flex;
align-items: center;
gap: .6rem;
flex-wrap: wrap;
font-size: .78rem;
color: var(--nn-05-muted);
min-inline-size: 0;
}
.nn-05__tag {
padding: .22rem .5rem;
border-radius: .35rem;
border: 1px solid var(--nn-05-line);
font-size: .64rem;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
}
.nn-05__tag--bad {
color: var(--nn-05-bad);
border-color: #4b2833;
}
.nn-05__tag--good {
color: var(--nn-05-neon);
border-color: color-mix(in srgb, var(--nn-05-neon) 40%, transparent);
}
.nn-05__well {
display: grid;
place-items: center;
min-height: clamp(10rem, 28vw, 13rem);
padding: clamp(1.5rem, 6vw, 2.75rem);
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
background: var(--nn-05-well);
overflow: hidden;
}
.nn-05__lamp {
position: relative;
padding: 1rem 1.4rem;
border-radius: .6rem;
border: 1px solid var(--nn-05-spec-neon);
background: #191125;
isolation: isolate;
}
.nn-05__lamp::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
box-shadow: 0 0 .35rem var(--nn-05-core), 0 0 clamp(.8rem, 3.5vw, 1.25rem) var(--nn-05-halo), 0 0 clamp(1.75rem, 8vw, 3rem) .2rem var(--nn-05-bloom);
z-index: -1;
}
.nn-05__lamp--raw::after {
opacity: var(--nn-05-raw);
animation: nn-05-raw var(--nn-05-dur) ease-in-out infinite;
}
.nn-05__lamp--reg::after {
opacity: var(--nn-05-i);
animation: nn-05-reg var(--nn-05-dur) ease-in-out infinite;
}
@keyframes nn-05-raw {
0%,
100% {
--nn-05-raw: .35;
}
50% {
--nn-05-raw: 1;
}
}
@keyframes nn-05-reg {
0%,
100% {
--nn-05-i: .35;
}
50% {
--nn-05-i: 1;
}
}
.nn-05__label {
font-family: ui-monospace, Menlo, monospace;
font-size: clamp(.8rem, 2.6vw, .92rem);
color: var(--nn-05-spec-ink);
letter-spacing: .02em;
}
.nn-05__mono {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .68rem;
line-height: 1.5;
color: var(--nn-05-muted);
overflow-wrap: anywhere;
}
.nn-05__code {
display: grid;
gap: .6rem;
padding: 1.1rem 1.25rem;
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
}
.nn-05__clabel {
margin: 0;
font-size: .7rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-05-muted);
}
.nn-05__pre {
margin: 0;
overflow-x: auto;
font-family: ui-monospace, Menlo, monospace;
font-size: .74rem;
line-height: 1.7;
color: var(--nn-05-neon);
}
.nn-05__cnote {
margin: 0;
font-size: .84rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__cnote strong {
color: var(--nn-05-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-05__lamp--raw::after,
.nn-05__lamp--reg::after {
animation: none;
opacity: 1;
}
}
```Here's a working CSS Neon Effect 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: Animating Glow Intensity with at-property
Source: https://codefronts.com/design-styles/css-neon/animating-glow-intensity-with-at-property/
An unregistered custom property cannot interpolate, so animating --glow steps between values instead of easing between them — the single most common "why won't my neon pulse" question. Registering it with @property and a typed syntax makes intensity animatable independently of hue. The broken version sits beside the working one.
## HTML
```html
<section class="nn-05" aria-labelledby="nn-05-heading">
<div class="nn-05__stage">
<div class="nn-05__theme">
<input class="nn-05__tin" type="checkbox" id="nn-05-theme">
<label class="nn-05__tlab" for="nn-05-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-05__head">
<p class="nn-05__kicker">Typed tokens</p>
<h2 class="nn-05__heading" id="nn-05-heading">A custom property has to be typed before it can breathe</h2>
<p class="nn-05__sub">Both lamps run the same keyframes on the same token name. Only one of them registered it.</p>
</header>
<div class="nn-05__compare">
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--bad">Unregistered</span>
Steps at the midpoint — reads as nothing happening
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--raw">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">--nn-05-raw: .35 → 1 / no @property</p>
</figure>
<figure class="nn-05__cell">
<figcaption class="nn-05__caption">
<span class="nn-05__tag nn-05__tag--good">Registered</span>
Interpolates — a real breath, hue unchanged
</figcaption>
<div class="nn-05__well">
<div class="nn-05__lamp nn-05__lamp--reg">
<span class="nn-05__label">Sync 4 repositories</span>
</div>
</div>
<p class="nn-05__mono">@property --nn-05-i { syntax: '<number>' }</p>
</figure>
</div>
<div class="nn-05__code">
<p class="nn-05__clabel">The registration</p>
<pre class="nn-05__pre"><code>@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}</code></pre>
<p class="nn-05__cnote">The token drives <strong>opacity on a glow layer that is built once</strong> — not the blur radius, and not the shadow colour. Intensity animates; the expensive part never repaints.</p>
</div>
</div>
</section>
```
## CSS
```css
@property --nn-05-i {
syntax: '<number>';
initial-value: 1;
inherits: false;
}
.nn-05 {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-spec-neon: #c2f24a;
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bad: #a12a20;
--nn-05-warn: #78490a;
}
.nn-05 {
--nn-05-spec-ink: #f4ecf7;
--nn-05-spec-dim: #a894b2;
}
.nn-05__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-05__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-05__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-05__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-05__tlab:hover {
opacity: 1;
}
.nn-05__tin:focus-visible + .nn-05__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
}
@media (prefers-color-scheme: dark) {
.nn-05:has(#nn-05-theme:checked) {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05 {
--nn-05-hue: 122;
--nn-05-dur: 3.6s;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
--nn-05-core: rgba(194, 242, 74, .9);
--nn-05-halo: rgba(194, 242, 74, .48);
--nn-05-bloom: rgba(194, 242, 74, .26);
--nn-05-raw: 1;
--nn-05-font-display: system-ui, "Helvetica Neue", Arial, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-05-bg);
color: var(--nn-05-ink);
font-family: system-ui, sans-serif;
}
.nn-05 *,
.nn-05 *::before,
.nn-05 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-05 {
--nn-05-bg: oklch(17% 0.035 310);
--nn-05-well: oklch(21% 0.045 310);
--nn-05-line: oklch(31% 0.055 310);
--nn-05-ink: oklch(96% 0.014 310);
--nn-05-muted: oklch(74% 0.03 310);
--nn-05-neon: oklch(90% 0.2 var(--nn-05-hue));
--nn-05-core: oklch(92% 0.2 var(--nn-05-hue) / .9);
--nn-05-halo: oklch(88% 0.19 var(--nn-05-hue) / .48);
--nn-05-bloom: oklch(84% 0.18 var(--nn-05-hue) / .26);
}
}
@media (prefers-color-scheme: light) {
.nn-05 {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
}
.nn-05[data-theme="light"] {
--nn-05-bg: #eceaef;
--nn-05-well: #241a2c;
--nn-05-line: #c8c2ce;
--nn-05-ink: #171320;
--nn-05-muted: #554b5e;
--nn-05-neon: #4f6b0a;
}
.nn-05[data-theme="dark"] {
--nn-05-bad: #f4a4a4;
--nn-05-warn: #f0c88a;
--nn-05-bg: #150d1a;
--nn-05-well: #1c1224;
--nn-05-line: #33203f;
--nn-05-ink: #f4ecf7;
--nn-05-muted: #a894b2;
--nn-05-neon: #c2f24a;
}
.nn-05__stage {
max-width: 66rem;
margin: 0 auto;
padding: clamp(1.5rem, 5vw, 2.25rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2rem, 6vw, 2.75rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-05__kicker {
margin: 0 0 .5rem;
font-size: .74rem;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--nn-05-neon);
}
.nn-05__heading {
margin: 0;
font-family: var(--nn-05-font-display);
font-size: clamp(1.4rem, 4.8vw, 2.1rem);
font-weight: 650;
letter-spacing: -.025em;
text-wrap: balance;
}
.nn-05__sub {
margin: .6rem 0 0;
max-width: 40rem;
font-size: .92rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__compare {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 52rem) {
.nn-05__compare {
grid-template-columns: 1fr 1fr;
}
}
.nn-05__cell {
margin: 0;
display: grid;
gap: .7rem;
min-inline-size: 0;
}
.nn-05__caption {
display: flex;
align-items: center;
gap: .6rem;
flex-wrap: wrap;
font-size: .78rem;
color: var(--nn-05-muted);
min-inline-size: 0;
}
.nn-05__tag {
padding: .22rem .5rem;
border-radius: .35rem;
border: 1px solid var(--nn-05-line);
font-size: .64rem;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
}
.nn-05__tag--bad {
color: var(--nn-05-bad);
border-color: #4b2833;
}
.nn-05__tag--good {
color: var(--nn-05-neon);
border-color: color-mix(in srgb, var(--nn-05-neon) 40%, transparent);
}
.nn-05__well {
display: grid;
place-items: center;
min-height: clamp(10rem, 28vw, 13rem);
padding: clamp(1.5rem, 6vw, 2.75rem);
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
background: var(--nn-05-well);
overflow: hidden;
}
.nn-05__lamp {
position: relative;
padding: 1rem 1.4rem;
border-radius: .6rem;
border: 1px solid var(--nn-05-spec-neon);
background: #191125;
isolation: isolate;
}
.nn-05__lamp::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
box-shadow: 0 0 .35rem var(--nn-05-core), 0 0 clamp(.8rem, 3.5vw, 1.25rem) var(--nn-05-halo), 0 0 clamp(1.75rem, 8vw, 3rem) .2rem var(--nn-05-bloom);
z-index: -1;
}
.nn-05__lamp--raw::after {
opacity: var(--nn-05-raw);
animation: nn-05-raw var(--nn-05-dur) ease-in-out infinite;
}
.nn-05__lamp--reg::after {
opacity: var(--nn-05-i);
animation: nn-05-reg var(--nn-05-dur) ease-in-out infinite;
}
@keyframes nn-05-raw {
0%,
100% {
--nn-05-raw: .35;
}
50% {
--nn-05-raw: 1;
}
}
@keyframes nn-05-reg {
0%,
100% {
--nn-05-i: .35;
}
50% {
--nn-05-i: 1;
}
}
.nn-05__label {
font-family: ui-monospace, Menlo, monospace;
font-size: clamp(.8rem, 2.6vw, .92rem);
color: var(--nn-05-spec-ink);
letter-spacing: .02em;
}
.nn-05__mono {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .68rem;
line-height: 1.5;
color: var(--nn-05-muted);
overflow-wrap: anywhere;
}
.nn-05__code {
display: grid;
gap: .6rem;
padding: 1.1rem 1.25rem;
border-radius: .75rem;
border: 1px solid var(--nn-05-line);
}
.nn-05__clabel {
margin: 0;
font-size: .7rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-05-muted);
}
.nn-05__pre {
margin: 0;
overflow-x: auto;
font-family: ui-monospace, Menlo, monospace;
font-size: .74rem;
line-height: 1.7;
color: var(--nn-05-neon);
}
.nn-05__cnote {
margin: 0;
font-size: .84rem;
line-height: 1.6;
color: var(--nn-05-muted);
text-wrap: pretty;
}
.nn-05__cnote strong {
color: var(--nn-05-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-05__lamp--raw::after,
.nn-05__lamp--reg::after {
animation: none;
opacity: 1;
}
}
```How this works
The browser needs a type before it can interpolate. A plain --glow: 0.4 is just a token string as far as the cascade is concerned. Animating it is legal, but since the engine does not know it is a number it animates it discretely — the value flips at the midpoint of each keyframe segment instead of easing. On screen that reads as a snap or a strobe, which is why people conclude the animation "does nothing".
@property supplies the type. Registering --nn-05-i with syntax: '<number>', an initial-value and inherits: false makes it a real animatable number. From that point a keyframe from 0.35 to 1 interpolates smoothly, and every property reading the token moves with it.
Point the token at opacity, not at the shadow. The glow stack is built once on a ::after layer and never changes; the animation only moves that layer's opacity. That keeps the expensive blur out of the per-frame work — the rule demo 07 measures. If you instead animate the shadow's blur or colour through the token you have simply found a more elegant way to repaint a large blurred area sixty times a second.
Why bother, if opacity alone would pulse? Because one typed token can drive several things at once — layer opacity, a border alpha, a text colour mix — and stay in step, while remaining a single value you can set per component or per state. That is the real payoff: intensity becomes a property of the component, tunable without touching hue.
Make it yours
- Change the keyframe's low end from
0.35to0.6for a shallower breath that never looks switched off. - Slow the cycle by editing
--nn-05-dur; the token is read by both specimens. - Retint with
--nn-05-hue— intensity is a separate axis, so the pulse depth does not change. - Add a second consumer of the token, such as the border colour, and both animate in lockstep.
- Register a second property for blur spread and animate it to see the paint cost demo 07 warns about.
- Set
inherits: trueon the registration to let a parent drive a whole group's intensity at once.
Gotchas — read before shipping
- Animating an unregistered custom property does not fail loudly — it steps at the segment midpoint, which most people read as the animation not running.
- Registering with
syntax: '*'gets you a registered property that still cannot interpolate; the syntax has to name a real type. - Driving the shadow's blur through the token instead of a layer's opacity repaints the blurred area every frame.
- A missing
initial-valuemakes the@propertyrule invalid, so the whole registration is dropped and you are silently back to discrete stepping.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 128+ | 114+ |
Floor set by :has(), oklch(), color-mix(), @property, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
@property sets the Safari and Firefox floors (16.4 and 128); oklch() sets the Chrome floor at 111. Without @property the token still resolves to its declared fallback and the glow renders at full intensity — the pulse steps rather than eases, which is exactly the failure the left-hand specimen demonstrates on purpose. The script-free theme toggle uses :has() (Chrome 105 / Safari 15.4 / Firefox 121) after the @media rules, so without it the demo follows the system colour scheme and the control does nothing.