DNA Helix
8 indigo/fuchsia dots at 12 px in 4 pairs, X-opposition oscillation 1 s (250 ms stagger between levels) — 60 × 60 px container in rotateY 2 s. Pure CSS.
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. Loaders has 29 effects, including 4 free. Explore the category →
3 usage examples



How it works
The component is a .loader-dna element of 60 × 60 px set to position: relative and transform-style: preserve-3d. It holds exactly 8 empty span elements arranged in 4 pairs: the first two at top: 0, the next at top: 25% (15 px), 50% (30 px) and 75% (45 px). Both spans in a pair share the same vertical position. Odd spans (:nth-child(2n+1)) get background: #6366f1 (indigo); even spans get #d946ef (fuchsia). All are round (border-radius: 50%), absolutely positioned at left: 50%, and a base transform: translateX(-50%) centers them — a value overridden by the animations the moment they start.
Two animations drive the lateral movement. dnaWave1 starts at translateX(-20px) scale(.8) at 0% and 100%, peaks at translateX(20px) scale(1) at 50%: the dot slides from left (smaller, at 80% scale) to right (full size). dnaWave2 is the exact reverse. These two keyframes alternate across odd/even pairs: at any moment, one dot moves left while its neighbor at the same level moves right — the two strands cross at the center of the container every 0.5 s. The four levels are offset by 0 s, 0.25 s, 0.5 s, 0.75 s: the delay spread covers exactly one full 1 s cycle distributed across 4 heights, creating a sinusoidal wave that travels down the structure.
A third animation, dnaRotate, applies rotateY(360deg) in 2 s linear infinite on the entire container. Thanks to transform-style: preserve-3d, all 8 children participate in the 3D context. No span has its own translateZ (Z = 0 for all): the depth comes entirely from the container's rotation. A measured consequence: at 90° and 270° of the rotation (0.5 s and 1.5 s into each 2 s cycle), the container is seen edge-on — the spans measure ~1 px wide (measured between 0.68 px and 0.85 px in Chromium). This “wire” state lasts about 80 ms per pass; it is the expected behavior of a flat object seen in profile, not an anomaly. The effect is entirely CSS — the js field is empty, no script is needed.
Accessibility
- prefers-reduced-motion not handled: all 3 animations (dnaRotate, dnaWave1, dnaWave2) run unconditionally, even when
prefers-reduced-motion: reduceis active. Measured in Playwright:animationPlayState: "running"on the container and all 8 spans after emulation. The CSS fix is two lines:@media (prefers-reduced-motion: reduce) { .loader-dna, .loader-dna span { animation: none } }. The integrator must decide on a visual fallback: 8 static stacked circles are enough to convey "something is present". - As a loading indicator, the component must signal its state to assistive technologies. The sold code contains no
role="status",aria-busy,aria-live, and no alternative text. Integrate as:<div class="loader-dna" role="status" aria-label="Loading">…</div>. If a visible "Loading…" text accompanies the loader,aria-labelis redundant —role="status"alone is sufficient. - The 8 empty
spanelements are presentational. Withoutaria-hidden="true"on the container, some screen readers traverse them and announce 8 empty elements. Addaria-hidden="true"on.loader-dnawhen the zone already carriesrole="status"and an accessible label. - Color contrast: both colors are non-text graphical components (WCAG 3:1 threshold for UI components). On the demo background
#0a0a0f: indigo#6366f1→ 5.4:1 ✓, fuchsia#d946ef→ 5.1:1 ✓. On white (#fff): indigo → 3.4:1 ✓, fuchsia → 3.0:1 (borderline — prefer#c026d3on light backgrounds to reach 3.3:1 without altering the hue). - The effect responds only to its CSS animation cycle: no mouse, keyboard or scroll-linked events are captured. The zone is not interactive and must not be focusable (no
tabindexin the sold code — do not add one).
Browser compatibility
Pure CSS: transform-style: preserve-3d, @keyframes, animation, border-radius: 50%, :nth-child. No external dependency, no JavaScript.
Without CSS animation support (very old browsers), the 8 circles render at their initial position — 4 overlapping pairs of static dots. Without transform-style: preserve-3d (IE 10 and below), the 3D rotation degrades to a 2D rotation: the effect remains legible but loses its depth. Colors and circular shapes are always rendered.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="loader-dna">
<span></span>
<span></span>
<!-- … 8 spans total — 4 pairs at top 0 / 25% / 50% / 75% -->
</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 |
|---|---|---|
Rotation duration (CSS — animation: 2s … dnaRotate) |
2 s | Speed of one full 3D revolution. 1s = lively rotation, sense of urgency; 4s = slow, deliberate rotation. Independent of the wave duration — the two cycles do not sync (2 s and 1 s: 2:1 ratio, the strands complete 2 crossings per revolution). |
Wave duration (CSS — animation: 1s … dnaWave1/dnaWave2) |
1 s | Speed of one back-and-forth sweep of a strand. Change all 8 animation declarations (spans 1–8) together. 0.6s = nervous strands; 2s = slow wave that accentuates depth. Do not go below 0.4s — the ease-in-out easing needs time to smooth the transitions. |
Level stagger (CSS — .25s, .5s, .75s) |
250 ms gap (4 levels × 0.25 s = 1 full cycle) | Vertical wave step. Lower to .1s: the wave descends faster and the helix looks more compact. Raise to .4s on a 1.6 s wave duration: the 4 levels then cover exactly one cycle (same ratio). Both spans of the same level always keep the same delay. |
Lateral amplitude (CSS — translateX(±20px)) |
±20 px | Range of oscillation from left: 50%. Maximum value without overflow: (container width / 2) − (span width / 2) = 30 − 6 = 24 px. At ±10px: a tighter helix with strands barely crossing; at ±24px: strands at the container edges. Change both keyframes (dnaWave1 and dnaWave2) together. |
Strand 1 color (CSS — background: #6366f1) |
#6366f1 (indigo) | Color of odd spans (positions 1, 3, 5, 7). Any valid CSS color value. A glow (box-shadow) is not present in the sold code — add manually if desired. |
Strand 2 color (CSS — background: #d946ef) |
#d946ef (fuchsia) | Color of even spans (positions 2, 4, 6, 8). Use a similar hue to strand 1 for a subtle monochrome helix; choose a complementary color to maximize strand contrast. |
Container size (CSS — width: 60px; height: 60px) |
60 × 60 px | The spans' top values are percentages (0 / 25% / 50% / 75%): they automatically adapt to a new height. Change width and height together to keep the square shape. Adjust the lateral amplitude (previous rule) proportionally. |
FAQ
dnaRotate animation turns the container 360° around the Y axis over 2 s. At 90° and 270° (0.5 s and 1.5 s into each cycle), the container is seen edge-on — the 8 spans, which have no depth of their own (translateZ: 0), appear as a line of ~1 px wide (measured: 0.68–0.85 px in Chromium). This state lasts about 80 ms per pass before the rotation continues. To avoid the "wire" effect, add translateZ(15px) to odd spans and translateZ(-15px) to even spans: the strands are placed in front of and behind the center plane, and the helix stays visible at every angle.transform-style: preserve-3d) while the children animate in 2D inside it — the combination of the two motions is what creates the helix illusion. Morphing Shape (fx-0431) and SVG Morphing Loaders (fx-0777) have no 3D rotation at all. DNA Helix is also the only one in this group that is entirely CSS without any SVG element.#6366f1 reaches 3.4:1 (WCAG — UI component ≥ 3:1 ✓) but fuchsia #d946ef drops to 3.0:1 (borderline — prefer #c026d3, 3.3:1). The #0a0a0f background in the sold code belongs to .demo-preview: remove that wrapper and apply your own background. .loader-dna itself has no declared background.