9 CSS Glow Effect Animations — Neon, Copy-Paste
A glowing button or headline looks lit from the inside like a neon sign, the trick gaming sites, streamers, and portfolios use to catch the eye.
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Neon text flicker
- 02 Button glow on hover
- 03 Border glow pulse
- 04 Card spotlight glow
- 05 Icon glow ring
- 06 Underline glow
- 07 @property glow color cycle
- 08 light-dark() aware glow
- 09 Glow intensity tied to scroll
The order below follows where a glow shows up on a real page, not popularity. 01 and 02 light up something someone is reading or about to click — a headline, then a button. 03 and 04 sit on cards, one signaling a live state and one following a cursor. 05 and 06 mark a spot — a notification icon, then which tab is active. 07 and 08 solve two problems that plain CSS colors can't: a smooth rainbow cycle and a glow that adapts to light or dark on its own. 09 closes with a glow that reacts to how far a card has scrolled into view.
01Neon text flicker
The letters look lit from the inside, brightening and dimming unevenly instead of holding one steady glow, the way a loosely wired neon tube actually behaves. Five layers of text-shadow stack up behind the color, and a flicker keyframe with thirteen uneven stops fades the whole stack's opacity in and out so the dips never fall into a predictable beat.
.neon {
text-shadow:
0 0 4px #ff2e63,
0 0 8px #ff2e63,
0 0 16px rgba(255, 46, 99, .8),
0 0 32px rgba(255, 46, 99, .6),
0 0 48px rgba(255, 46, 99, .35);
animation: flicker 2s linear infinite;
}
@keyframes flicker {
0% { opacity: 1; }
6% { opacity: .3; }
14% { opacity: 1; }
20% { opacity: 1; }
26% { opacity: .4; }
34% { opacity: 1; }
50% { opacity: 1; }
56% { opacity: .25; }
64% { opacity: 1; }
80% { opacity: 1; }
86% { opacity: .35; }
94% { opacity: 1; }
100% { opacity: 1; }
}
02Button glow on hover
A button carries a faint halo at rest, then the light spreads wider and brighter the instant a cursor hovers over it or keyboard focus lands on it. Two stacked box-shadow layers grow in blur and opacity on :hover and :focus-visible together, so a mouse click and a Tab key press trigger the exact same light.
.btn {
box-shadow: 0 0 8px rgba(0, 229, 255, .3),
0 0 16px rgba(0, 229, 255, .12);
transition: box-shadow .3s ease-out, transform .3s ease-out;
}
.btn:hover,
.btn:focus-visible {
box-shadow: 0 0 12px #00e5ff,
0 0 32px rgba(0, 229, 255, .65),
0 0 48px rgba(0, 229, 255, .3);
transform: scale(1.06);
}
03Border glow pulse
A ring of light traced around a card's border breathes wider and dimmer every couple of seconds, like a heartbeat telling a viewer this card is live right now rather than sitting idle. One box-shadow keyframe swings the blur radius and opacity between a quiet resting state and a bright peak, and the border's position never shifts while it does.
.card { animation: pulse 2s ease-in-out infinite; }
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 rgba(57, 255, 136, 0), 0 0 8px rgba(57, 255, 136, .18);
border-color: rgba(57, 255, 136, .35);
}
50% {
box-shadow: 0 0 4px rgba(57, 255, 136, .7),
0 0 32px rgba(57, 255, 136, .55),
0 0 48px rgba(57, 255, 136, .22);
border-color: rgba(57, 255, 136, .9);
}
}
04Card spotlight glow
A patch of colored light drifts in a slow loop across a card until a cursor lands on it, at which point the light stops looping and follows the pointer instead. A radial-gradient reads its center from two custom properties, --x and --y, and those only interpolate smoothly through @keyframes because @property registers them as a <percentage> first — an unregistered custom property would just jump from one value to the next with no motion in between.
@property --x { syntax: '<percentage>'; inherits: false; initial-value: 50%; }
@property --y { syntax: '<percentage>'; inherits: false; initial-value: 50%; }
.card::before {
content: '';
position: absolute; inset: 0;
background: radial-gradient(circle at var(--x) var(--y),
rgba(179, 107, 255, .55), transparent 55%);
animation: sweep 2s ease-in-out infinite;
}
@keyframes sweep {
0% { --x: 18%; --y: 20%; }
25% { --x: 85%; --y: 22%; }
50% { --x: 78%; --y: 80%; }
75% { --x: 20%; --y: 76%; }
100% { --x: 18%; --y: 20%; }
}
05Icon glow ring
Two rings expand outward from a small icon and fade to nothing, one starting half a beat after the other, reading like a signal broadcasting outward the way a live or notification dot often does. Both rings only animate scale and opacity on a staggered animation-delay, which makes this the lightest-weight glow in the whole set.
.ring {
position: absolute; inset: 0;
border-radius: 50%;
border: 2px solid #ffb020;
opacity: 0;
}
.ring--a { animation: ring 2s ease-out infinite; }
.ring--b { animation: ring 2s ease-out infinite; animation-delay: 1s; }
@keyframes ring {
0% { transform: scale(.7); opacity: .8; }
70% { transform: scale(1.7); opacity: 0; }
100% { transform: scale(1.7); opacity: 0; }
}
06Underline glow
A glowing underline slides beneath whichever tab is active, trailing a soft box-shadow behind it instead of snapping straight into place. Both tabs sit at a fixed 96px width, so the underline's travel distance is one fixed number inside a translateX keyframe rather than something measured off the rendered page after the fact.
.tab { flex: 0 0 auto; width: 96px; }
.indicator {
position: absolute; left: 0; bottom: -8px;
width: 96px; height: 3px;
background: linear-gradient(90deg, transparent, #2df5c0, transparent);
box-shadow: 0 0 12px #2df5c0, 0 0 24px rgba(45, 245, 192, .6);
animation: slide 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes slide {
0%, 40% { transform: translateX(0); }
60%, 90% { transform: translateX(112px); }
100% { transform: translateX(0); }
}
07@property glow color cycle
A round badge's glow drifts through the whole color wheel on a two-second loop, never landing on the same shade twice in a cycle. That smooth sweep only works because --hue is registered through @property as a <number>, letting hsl() interpolate the hue angle instead of jumping straight from one color to the next.
@property --hue { syntax: '<number>'; inherits: false; initial-value: 262; }
.badge {
width: 80px; height: 80px;
border-radius: 50%;
box-shadow: 0 0 16px #8b5cf6, 0 0 40px rgba(139, 92, 246, .5);
animation: cycle 2s linear infinite;
}
@supports (background: hsl(var(--hue) 100% 50%)) {
.badge {
box-shadow: 0 0 16px hsl(var(--hue) 90% 62%),
0 0 40px hsl(var(--hue) 90% 55% / .55);
}
}
@keyframes cycle {
from { --hue: 0; }
to { --hue: 360; }
}
08light-dark() aware glow
The same kind of badge glows faintly on a light background and strongly on a dark one, with no media query anywhere in the file and no JavaScript checking which theme is active. Once color-scheme is set on the surrounding element, writing the glow's color through light-dark() lets that one line pick the right shade for itself on either background.
.pane--light { color-scheme: light; }
.pane--dark { color-scheme: dark; }
.badge {
--glow-rest: light-dark(rgba(124, 92, 255, 0), rgba(124, 92, 255, .2));
--glow-peak: light-dark(rgba(124, 92, 255, .7), #7c5cff);
box-shadow: 0 0 4px var(--glow-rest);
animation: breathe 2s ease-in-out infinite;
}
@keyframes breathe {
0%, 15%, 85%, 100% { box-shadow: 0 0 4px var(--glow-rest); }
35%, 65% { box-shadow: 0 0 16px var(--glow-peak); }
}
09Glow intensity tied to scroll
A card's glow builds from barely-there to fully lit as it nears the center of the screen while scrolling, then fades back down once it drifts past. An IntersectionObserver writes the intersection ratio straight into a registered --intensity custom property, and both box-shadow and opacity read that same value through calc(), so the glow and the fade always move together.
@property --intensity { syntax: '<number>'; inherits: false; initial-value: 0; }
.card {
opacity: calc(.45 + var(--intensity) * .55);
box-shadow: 0 0 calc(var(--intensity) * 12px) #3ec9ff,
0 0 calc(var(--intensity) * 40px) rgba(62, 201, 255, .55);
transition: box-shadow .3s linear, opacity .3s linear;
}
Where does light-dark() break?
Item 08's badge only ever feeds light-dark() a <color> — the resting glow, the peak glow, the panel background. The first build of this demo also tried feeding it a length, swapping the blur radius between a smaller value on light panels and a wider one on dark panels. That version didn't render a dimmer glow on the wrong background — it rendered no glow at all, because light-dark() outside a <color> value doesn't fall back to either side quietly — it makes the entire box-shadow declaration invalid, and an invalid declaration gets dropped by the browser with no console warning. A 24-frame capture of that broken version measured zero frames of motion before the fix went in: keep light-dark() inside colors only, and leave every length — blur, spread, radius — as a plain number outside it. The full SCSS and React source for all nine glows, including the exact line that broke item 08 before the fix, opens with azk2eev6, typed exactly as it reads here. One more thing worth knowing before copying any of this: box-shadow repaints every frame instead of compositing on the GPU the way transform and opacity do, so stacking more than two or three layers — 01, 03, and 04 already sit at that edge — is the kind of choice that costs frame rate first on a low-end phone.
Variants worth trying
| Variant | Changed value | Feel |
|---|---|---|
| Softer spotlight | Item 04's gradient stop cut from 55% to 35% |
The light pools tighter around the cursor instead of washing across the whole card |
| Urgent flicker | Item 01's 2s loop shortened to 800ms |
Reads like a warning light rather than a lazily buzzing neon tube |
| Brand-locked cycle | Item 07's --hue keyframes limited to 200 through 260 |
Stays inside blue-violet the whole loop instead of crossing the full rainbow |
Accessibility
All nine drop their animation to none under prefers-reduced-motion: reduce, but the glow itself — the color, the shadow — stays on the page; only the motion goes quiet. 01 settles at a steady full-opacity glow instead of flickering, 03 holds one steady mid-brightness glow instead of pulsing, and 04, 07, and 09 freeze their @property values at a sensible default — the spotlight centers itself, the hue cycle parks on one violet, and the scroll glow reads as fully lit rather than snapping to zero. Browser coverage is the other limit worth knowing: @property reaches Chrome/Edge 85+ and Safari 16.4+, but Firefox only added it in version 128, and light-dark() needs Chrome/Edge 123+, Firefox 120+, or Safari 17.5+. Older engines skip 04, 07, 08, and 09's animated part and fall back to the static glow color already written into the CSS — nothing breaks, it just stops moving. More hover and click effects in the same copy-paste format live in 9 CSS button animations, and the rest of what this site has published sits in the CSS category hub.
FAQ
Do I need a glow effect generator, or is this short enough to write by hand?
Every rule above is six to twenty lines of plain SCSS, short enough to type or paste without a generator tool. A generator is worth reaching for when you need dozens of one-off variations fast, but it usually outputs more shadow layers and vendor cruft than any of the nine here actually need.
Why does my text-shadow neon effect look flat instead of glowing?
A single text-shadow layer reads as a hard-edged outline, not a glow — glow comes from stacking several layers at increasing blur radius and decreasing opacity, the way item 01 stacks five. Skipping the outer, most-blurred layers is the most common reason a "neon" text effect ends up looking like plain colored text with a shadow.
What's the real difference between text-shadow and box-shadow for a glow?
text-shadow only follows the shape of glyphs, so it's the right choice for glowing letters like item 01. box-shadow follows an element's box (or its border-radius), which is why every glow on a button, card, or badge in this set — 02 through 09 — uses box-shadow instead.