Glow Button in React: CSS Box Shadow Animation on Hover
Build a glow button in React using CSS box-shadow animation on hover. Real code, Tailwind v4 examples, and zero-flicker transitions you'll actually want to ship.
Why Glow Buttons Still Hit Hard in 2026
Honestly, the glow button is one of those effects that never actually went out of style — it just migrated from "flashy" to "intentional." Dark-mode UIs, glassmorphism dashboards, SaaS products that want energy without noise — they all reach for it. And for good reason.
The mechanic is simple: you animate a box-shadow on hover to produce a colored halo effect around the button. No canvas, no WebGL, no JavaScript at all if you do it right. Pure CSS. That makes it fast, accessible by default, and easy to drop into any React component without blowing up your bundle.
This article walks you through building one from scratch — plain CSS first, then a Tailwind v4.0.2 variant — and explains the exact values that make the difference between a glow that looks polished and one that looks like a MySpace profile from 2006.
How CSS box-shadow Actually Produces the Glow
box-shadow accepts multiple comma-separated layers. That's the whole trick. A single hard shadow looks like a drop shadow. Stack three or four layers with increasing spread and decreasing opacity, and you get a bloom — light spilling outward, soft at the edges, bright at the center.
Here's the anatomy of a good glow stack. The innermost layer has zero blur and zero spread, just to anchor the color right on the border. The middle layers do the heavy lifting: 0px 0px 20px 4px rgba(139,92,246,0.6) gives you a solid core glow. The outermost layer, something like 0px 0px 60px 16px rgba(139,92,246,0.15), is the wide, barely-visible bloom that makes it feel expensive.
Opacity is everything. rgba(139,92,246,0.6) on the inner layer and rgba(139,92,246,0.15) on the outer one. Blow those ratios and the glow either looks flat or looks radioactive. Start with 0.6 and 0.15, then adjust to taste for your specific background color.
Building the Glow Button Component in React
Let's write this out properly. The component is a styled button that switches between a rest state (no shadow or a very subtle one) and a hover state with the full glow stack. We'll handle the transition in CSS so React doesn't need to track anything — no useState, no ref, nothing.
// GlowButton.tsx
import React from 'react';
import './GlowButton.css';
interface GlowButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
glowColor?: string;
children: React.ReactNode;
}
export function GlowButton({
glowColor = '#8B5CF6',
children,
className = '',
...props
}: GlowButtonProps) {
return (
<button
className={`glow-btn ${className}`}
style={{ '--glow-color': glowColor } as React.CSSProperties}
{...props}
>
{children}
</button>
);
}We're passing glowColor as a CSS custom property so the stylesheet can reference it without needing inline styles for the shadow itself. That keeps the CSS doing CSS things and the React doing React things.
The CSS: Transitions, Layers, and the Zero-Flicker Rule
/* GlowButton.css */
.glow-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 24px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--glow-color) 40%, transparent);
background: color-mix(in srgb, var(--glow-color) 12%, transparent);
color: #fff;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
box-shadow:
0 0 0 0 transparent,
0 0 0 0 transparent,
0 0 0 0 transparent;
transition:
box-shadow 300ms ease,
background 300ms ease,
border-color 300ms ease;
}
.glow-btn:hover {
background: color-mix(in srgb, var(--glow-color) 20%, transparent);
border-color: color-mix(in srgb, var(--glow-color) 70%, transparent);
box-shadow:
0 0 8px 2px color-mix(in srgb, var(--glow-color) 80%, transparent),
0 0 24px 6px color-mix(in srgb, var(--glow-color) 40%, transparent),
0 0 60px 12px color-mix(in srgb, var(--glow-color) 15%, transparent);
}
.glow-btn:focus-visible {
outline: 2px solid var(--glow-color);
outline-offset: 3px;
}
.glow-btn:active {
transform: scale(0.97);
transition-duration: 80ms;
}The zero-flicker rule: always define box-shadow on the rest state, even if it's all zeros. Browsers need matching shadow counts to interpolate between states. If you go from box-shadow: none to three layers, some browsers will snap instead of animate. Three zeros to three values — smooth every time.
Notice color-mix() instead of hardcoded rgba values. This is a CSS Level 4 feature that's been in all major browsers since mid-2023. It means you can derive opacity variants directly from the custom property without needing Sass or PostCSS. Much cleaner when your design system hands you brand tokens.
Tailwind v4 Approach: Utility Classes and Arbitrary Values
If you're on Tailwind v4.0.2 and don't want a separate CSS file, arbitrary value syntax covers you. The trade-off is verbosity in JSX — but for a one-off button in a landing page, it's often the faster path.
export function GlowButtonTailwind({ children }: { children: React.ReactNode }) {
return (
<button
className="
inline-flex items-center justify-content-center
px-6 py-2.5 rounded-lg
border border-violet-500/40
bg-violet-500/10
text-white text-[0.9375rem] font-medium
cursor-pointer
shadow-[0_0_0_0_transparent]
transition-all duration-300 ease-out
hover:bg-violet-500/20
hover:border-violet-500/70
hover:shadow-[0_0_8px_2px_rgba(139,92,246,0.8),0_0_24px_6px_rgba(139,92,246,0.4),0_0_60px_12px_rgba(139,92,246,0.15)]
focus-visible:outline-2 focus-visible:outline-violet-500 focus-visible:outline-offset-2
active:scale-[0.97] active:duration-75
"
>
{children}
</button>
);
}The shadow-[...] arbitrary value with comma-separated layers is the key move here. Tailwind v4 handles the comma escaping correctly — earlier versions didn't, which caused silent failures. If you're on v3.x, you'll need to wrap the whole shadow value in CSS escape syntax or just use a stylesheet.
You might wonder: is this approach worth it compared to a dedicated CSS file? For a design system component you'll reuse everywhere, no — the CSS file is cleaner and more maintainable. For a hero section CTA you build once and move on, the Tailwind approach is fine. Pick your tool for the job.
Color Theming and the Multi-Color Glow Trick
A single-color glow is good. A two-color glow is better. Think: primary color on the inner layers, a shifted hue (15–30 degrees offset) on the outermost bloom. It creates a prismatic edge that reads as high-quality rather than simple. You see this constantly in premium SaaS products — Vercel's buttons, Linear's CTAs.
.glow-btn--prismatic:hover {
box-shadow:
0 0 8px 2px rgba(139, 92, 246, 0.8),
0 0 24px 6px rgba(139, 92, 246, 0.4),
0 0 60px 12px rgba(99, 102, 241, 0.2),
0 0 90px 24px rgba(59, 130, 246, 0.1);
}The outer two layers shift from violet (139,92,246) to indigo (99,102,241) to blue (59,130,246). At those opacities — 0.2 and 0.1 — the shift is barely perceptible consciously, but it makes the glow feel three-dimensional. This is the kind of detail that separates components you find in free glassmorphism component libraries from ones you actually ship to production.
Dark backgrounds amplify everything. If your app uses rgba(255,255,255,0.15) or similar translucent backgrounds — common in glassmorphism UIs — the glow will read much stronger than on a solid dark surface. Test on both #0f0f0f and rgba(255,255,255,0.05) to calibrate your opacity values.
Accessibility and Performance Considerations
Box shadow animations run on the compositor thread in modern browsers — they don't trigger layout or paint. That means they're essentially free from a performance standpoint. You don't need will-change: box-shadow unless you're animating dozens of buttons simultaneously, which you probably aren't.
Accessibility is a different story. The glow effect itself doesn't affect screen readers or keyboard nav, but make sure you're not removing the default focus ring and replacing it with nothing. The .glow-btn:focus-visible rule in our CSS above handles that — it shows a clean 2px outline only when navigating by keyboard, not on mouse click. That's the correct pattern.
Also worth pairing this with a theme toggle in React if your app supports light mode. A violet glow on a white background looks terrible — you'll want to either desaturate it, reduce opacity significantly, or swap to a border-only focus state. CSS custom properties make that straightforward: one variable change in your light theme and everything adjusts.
Motion preferences matter too. Wrap the hover transition in a @media (prefers-reduced-motion: no-preference) block so users who've opted out of animations don't see flashing glow on hover. It's two lines of CSS and it's the right thing to do.
Combining Glow Buttons With Other Visual Styles
Glow buttons pair naturally with dark visual aesthetics — neumorphism, glassmorphism, and neobrutalism all have flavors that accommodate them. The interesting case is neobrutalism, where the glow acts as contrast against hard black borders and flat fills. It shouldn't work in theory, but it does.
What doesn't work: glow buttons inside claymorphic UIs. The whole point of claymorphism is soft, inflated, pastel surfaces. A sharp glowing CTA reads as tonally wrong. Stick to inset shadows and color-pop backgrounds in those contexts instead.
Empire UI ships glow buttons as part of its Neon and Cyberpunk style presets, but the underlying component accepts any color token. If you've already picked a CSS variable scheme for your app — something like --color-brand-primary — you can pass that directly as glowColor and it'll adapt. No one-off hardcoded values needed.
FAQ
You're almost certainly missing a box-shadow declaration on the rest state. Browsers can only interpolate between shadow layers when the count matches. Set box-shadow: 0 0 0 0 transparent, 0 0 0 0 transparent, 0 0 0 0 transparent on your base state — one zero-layer for each layer in your hover state — and the transition will be smooth.
You need arbitrary values. Tailwind's built-in shadow utilities (shadow-sm, shadow-lg, etc.) only generate single-layer box shadows and use fixed gray-black colors. For a colored multi-layer glow you'll use shadow-[0_0_8px_2px_rgba(139,92,246,0.8),0_0_24px_...] syntax. On Tailwind v4.0.2 this works correctly — on v3.x you may hit comma-escaping issues.
No, not in practice. Box shadow changes are composited — they don't trigger layout recalculation or paint in modern browsers. You'd need to be animating hundreds of elements simultaneously before it registers as a perf concern. For a button or two on a page, it's essentially free.
Reduce the outer layer opacities dramatically and shift to a border glow rather than a spread glow. On white or light gray backgrounds, anything above rgba(x,x,x,0.3) on the outer layers will look muddy. A rule of thumb: halve all your opacity values for light mode. Using CSS custom properties for the color makes this a one-variable change in your light theme block.
filter: drop-shadow() follows the element's alpha shape, which matters for non-rectangular elements like SVG icons. For a standard button with a border-radius, box-shadow is the right tool — it's more predictable, supports multiple layers natively, and animates more efficiently since it avoids triggering a filter re-composite on every frame.
Wrap your transition declaration in @media (prefers-reduced-motion: no-preference) { .glow-btn { transition: box-shadow 300ms ease; } }. This means users who've enabled reduced motion in their OS won't see the animated glow on hover. The visual state change on hover can still exist — just without the transition timing.