Neon Glow Bars
15 DOM bars in cyan / pink / purple pulse on a dual-sine audio simulation — 3-layer CSS neon glow (5–40 px) driven by a single rAF loop, no canvas.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Atmosphere has 57 effects, including 10 free. Explore the category →
3 usage examples



How it works
Each bar is a <div> 10 px wide with border-radius: 5px 5px 0 0. The neon is rendered entirely in CSS: a triple-layer box-shadow using the same color — 10 px (inner halo), 20 px (diffusion), 40 px (wide bloom) — that JS rewrites every frame to make the intensity breathe with the signal. No canvas, no SVG.
The simulated signal comes from getSimulatedAudio(15, time, 1.5), which produces for each bar i the value |sin(t × freq × 0.002) × 0.5 + sin(t × freq × 0.003 + i) × 0.3 + rand × 0.2|, where freq = 1.5 + i × 0.5. Two sinusoids at different phase speeds per bar create a convincing EQ shape without an AudioContext; the 20 % random noise adds organic micro-variation.
The glow intensity is 0.5 + data[i] × 0.5 (range 0.5–1.0). Box-shadow radii are multiplied by this value: 0 0 ${10×intensity}px color, etc. When the bar is low, the halo dims to half; at peak, it reaches its full 40 px bloom. Bar height follows 10 + data[i] × 100 px (range 10–110 px) inside a 120 px container.
The CSS adds transition: height .05s to each bar, smoothing the jump between rAF frames without extra JS. The main loop — a single shared requestAnimationFrame — calls animateNeonBars(time) with the DOMHighResTimeStamp, keeping the animation framerate-stable.
Accessibility
- prefers-reduced-motion absent: the code does not check this media query — the animation runs continuously even when the OS has reduced-motion enabled. Add manually for WCAG 2.1 SC 2.3.3 compliance:
if (window.matchMedia('(prefers-reduced-motion:reduce)').matches) return;before therequestAnimationFramecall. - No aria-hidden on the
.neon-barscontainer — screen readers traverse the bar elements with no useful announcement. Addaria-hidden="true"whenever the bars are purely decorative. - No role or aria-label on the scene. Depending on context, wrap in
role="img"with a descriptivearia-label(e.g. 'Simulated audio visualizer'). - The bars carry no text — no contrast issue on the animated elements themselves. Verify contrast of any overlay text added on top (WCAG AA ratio ≥ 4.5:1).
- No keyboard interactivity on the bars — they do not capture focus or intercept keyboard events.
Browser compatibility
Requires CSS box-shadow, requestAnimationFrame, and DOM manipulation — supported in all modern browsers. No canvas or WebGL needed.
If JS is disabled, the initial HTML bars remain displayed statically with their inline styles. No errors, the page stays functional.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="neon-bars" id="neonBars" aria-hidden="true">
<!-- Bars created by JS — 15 × cycling cyan / pink / purple -->
<div class="bar cyan"></div>
<div class="bar pink"></div>
<div class="bar purple"></div>
<!-- … 12 more bars … -->
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| CSS .neon-bars .bar.cyan { background: … } | #06b6d4 | Fill color and glow tint for cyan bars. Update the background and all 3 box-shadow color values (4 occurrences total). |
| CSS .neon-bars .bar.pink { background: … } | #d946ef | Pink bar color. Replace all 4 occurrences of #d946ef to retheme the full channel. |
| CSS .neon-bars .bar.purple { background: … } | #8b5cf6 | Purple bar color. Same — 4 occurrences in the CSS. |
| CSS .neon-bars { height: 120px } | 120px | Container height = ceiling of the tallest bar. Increase for a more dramatic effect, reduce for a discreet integration. |
| CSS .neon-bars { gap: 6px } | 6px | Horizontal spacing between bars. 2–3 px gives a dense clustered look; 10–12 px a more spread-out spectrum. |
| JS neonBarCount = 15 | 15 | Number of bars created. Useful range: 8 (subtle) to 32 (dense spectrum). Change before DOM creation in the Audio Visualizer section. |
| JS getSimulatedAudio(…, 1.5) | 1.5 | 3rd argument = base sine frequency. 0.5 = slow, undulating motion; 3.0 = fast, nervous energy. No effect on color. |
| JS const intensity = 0.5 + data[i] * 0.5 | 0.5 + data[i] * 0.5 | Glow intensity formula. Use 0.8 + data[i] * 0.2 for always-on bright bloom; 0.2 + data[i] * 0.8 for high-contrast peak vs. trough. |
FAQ
getSimulatedAudio() overlays two sinusoids at different phase speeds per bar, plus 20% random noise. To connect a real FFT analyser, replace the returned values with data from an AnalyserNode.getByteFrequencyData() normalized to 0–1.requestAnimationFrame(animate) with const rafId = requestAnimationFrame(animate) — then call cancelAnimationFrame(rafId) when needed (component unmount, out-of-view, etc.).neonBarCount is read at init. To reconfigure: clear the container (neonBars.innerHTML = ''), update the constant, recreate bars with the init for loop. The already-running rAF loop will resume animating the new DOM without restart.