3D Bars
8 CSS bars in 3D perspective animated by sinusoidal synthesis — per-bar oscillating rotateY and a double offset box-shadow simulate depth without 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
The .bars-3d container enables the 3D scene via perspective: 500px and transform-style: preserve-3d. Each bar gets a fixed rotateX(-10deg) — tilting it slightly toward the viewer — and a dynamic rotateY centered on −15°, exposing the left face and creating the illusion of a solid block.
Each frame, getSimulatedAudio(8, time, baseFreq=2) generates 8 values in [0–1] by summing two sinusoids whose frequency increases by +0.5 per bar index, plus 20% random noise. Bar height maps to 20 + value × 80 px (range 20 → 100 px). The different per-bar frequencies break synchrony and produce organic motion.
Visual depth relies on two solid (zero-blur) box-shadow values: rgba(79,70,229,.5) 4px 4px 0 and rgba(79,70,229,.3) 8px 8px 0. These right-and-downward offsets simulate the right lateral face and top of each bar — the same depth a real 3D model would require additional geometry for.
The rotateY oscillation follows -15 + Math.sin(time × 0.002 + i × 0.5) × 5 degrees: the i × 0.5 term phase-shifts each bar by index, producing a progressive left-to-right rotation wave. All page animations share a single requestAnimationFrame; no IntersectionObserver is present — the loop runs continuously.
Accessibility
- prefers-reduced-motion not implemented: the code does not check this media query — the RAF loop runs indefinitely regardless of the OS setting. Add a check before deploying to an accessible context (e.g.
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;). - The
.bars-3delement carries norole,aria-label, oraria-hidden. For a purely decorative use, addaria-hidden="true"to the root container so screen readers skip the bars. - No pause/stop control is exposed — WCAG 2.2.2 (pause mechanism for any auto-started animation lasting more than 5 s) is not met out of the box.
- Bars are plain non-focusable
<div>elements with no semantic role — correct for pure decoration, but add appropriate semantics if the bars carry real data (e.g. dashboard metrics). - Color contrast: the indigo-violet gradient on a
#0a0a0fbackground exceeds the 3:1 ratio required for non-text graphical elements (WCAG 1.4.11 — compliant for a decorative element).
Browser compatibility
Requires CSS 3D transforms (perspective, transform-style, rotateX/Y) and requestAnimationFrame — available since Chrome 36+, Firefox 16+, Safari 9+. Zero external dependencies.
If JS is disabled, the 8 bars render statically with the inline transforms and heights from the initial HTML — the visual is preserved, frozen. No fatal JS errors.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<div class="bars-3d" id="bars3d" aria-hidden="true">
<!-- 8 <div class="bar"> created dynamically by the JS -->
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
</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 |
|---|---|---|
| `.bars-3d .bar` — background | linear-gradient(135deg, #6366f1, #8b5cf6, #4f46e5) | Bar color and gradient direction. Swap the three hex values for your brand palette; the 135° angle orients the light top-left → bottom-right. |
| `.bars-3d` — perspective | 500px | CSS 3D scene depth. 300 px = dramatic and exaggerated; 800–1000 px = subtle tilt, nearly flat. |
`bars3dCount` (JS, line const bars3dCount = 8) |
8 | Number of bars created at startup. 4 bars = clean minimal chart; 16 bars = dense visualizer (reduce width and gap accordingly). |
| `.bars-3d .bar` — width | 20px | Width of each bar. Combine with gap to balance negative space for the container's total width. |
| `.bars-3d` — gap | 8px | Spacing between bars. 4 px = tight and dense; 14 px = airy and minimal. |
`time × 0.002` in rotateY (JS, animateBars3d) |
0.002 | Rotation oscillation speed per bar. 0.001 = slow wave; 0.004 = rapid shimmer. |
`20 + data[i] × 80` in animateBars3d (JS) |
min 20 px, amplitude 80 px | Height range. 10 + data[i] × 100 = bars nearly flush at rest, full dynamics; 40 + data[i] × 40 = restrained movement around a fixed midpoint. |
| `.bars-3d .bar` — box-shadow | rgba(79,70,229,.5) 4px 4px 0, rgba(79,70,229,.3) 8px 8px 0 | Offset shadows simulating the lateral faces of the 3D block. Increase offsets (6 px / 12 px) for more depth; match the color to keep it consistent with the gradient. |
FAQ
perspective, transform-style: preserve-3d, and rotateX/rotateY on ordinary <div> elements. Depth is reinforced by two solid zero-blur box-shadow values. Zero canvas, zero WebGL, zero external dependencies.getSimulatedAudio() with your real source — for example a Web Audio API buffer (analyser.getByteFrequencyData(buf)) or a normalized array from your own API. The animateBars3d function only needs an array of values between 0 and 1: adjust height = 20 + data[i] * 80 accordingly.IntersectionObserver in the current implementation. The requestAnimationFrame loop runs continuously. To save CPU when the element is off-screen, attach an observer to .bars-3d that sets a boolean paused flag and returns early from animateBars3d when true.