Pure CSS Loaders & patterns #loop

DNA helix in pure CSS

Rungs spin on negative delays; dots counter-rotate so they always face the camera.

  • 42 lines of CSS
  • 16 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Every rung runs the same rotateY animation.
  2. A negative animation-delay per rung starts each one part-way through its turn — that offset is the twist.
  3. Flat dots would vanish when seen edge-on, so each dot runs the same animation in reverse. The two rotations cancel and the dot always faces the camera (a "billboard").
  4. The rung needs preserve-3d for that cancellation to happen in 3D space.
  5. Every length is a multiple of one base unit, --u, tied to the canvas: a rung is 120 units wide and the twelve of them stand 12 apart, so the helix is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • negative animation-delay
  • billboard counter-rotation
  • delay from --i per rung

Copy-paste code

HTML 16 lines

<div class="scene">
  <div class="helix">
    <div style="--i:1"><i></i><i></i></div>
    <div style="--i:2"><i></i><i></i></div>
    <div style="--i:3"><i></i><i></i></div>
    <div style="--i:4"><i></i><i></i></div>
    <div style="--i:5"><i></i><i></i></div>
    <div style="--i:6"><i></i><i></i></div>
    <div style="--i:7"><i></i><i></i></div>
    <div style="--i:8"><i></i><i></i></div>
    <div style="--i:9"><i></i><i></i></div>
    <div style="--i:10"><i></i><i></i></div>
    <div style="--i:11"><i></i><i></i></div>
    <div style="--i:12"><i></i><i></i></div>
  </div>
</div>

CSS 42 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the helix is the same share of a
     gallery card, the editor, a full screen and a recording canvas */
  --u: 0.29vmin;
  perspective: calc(800 * var(--u));
}

.helix {
  display: grid;
  gap: calc(12 * var(--u));
  transform-style: preserve-3d;
  transform: rotateZ(-14deg);
}

.helix > div {
  --delay: calc(var(--i) * -0.22s);
  position: relative;
  width: calc(120 * var(--u));
  height: calc(3 * var(--u));
  background: linear-gradient(90deg, #2ee6d6, transparent 35% 65%, #ff4d9d);
  transform-style: preserve-3d;
  animation: helix-spin 4s linear infinite;
  animation-delay: var(--delay);
}

.helix i {
  position: absolute;
  top: calc(-6 * var(--u));
  width: calc(15 * var(--u));
  height: calc(15 * var(--u));
  border-radius: 50%;
  /* same animation, reversed → the dot always faces the camera */
  animation: helix-spin 4s linear infinite reverse;
  animation-delay: var(--delay);
}

.helix i:first-child { left: calc(-7 * var(--u));  background: #2ee6d6; box-shadow: 0 0 calc(10 * var(--u)) #2ee6d6; }
.helix i:last-child  { right: calc(-7 * var(--u)); background: #ff4d9d; box-shadow: 0 0 calc(10 * var(--u)) #ff4d9d; }

@keyframes helix-spin {
  to { transform: rotateY(360deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.