Pure CSS Loaders & patterns #loop #sass-loop

DNA helix in pure CSS

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

  • 39 lines of CSS
  • 16 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed
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.

Key techniques

  • negative animation-delay
  • billboard counter-rotation
  • Sass @for

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 39 lines

.scene {
  perspective: 800px;
}

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

.helix > div {
  --delay: calc(var(--i) * -0.22s);
  position: relative;
  width: 120px;
  height: 3px;
  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: -6px;
  width: 15px;
  height: 15px;
  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: -7px;  background: #2ee6d6; box-shadow: 0 0 10px #2ee6d6; }
.helix i:last-child  { right: -7px; background: #ff4d9d; box-shadow: 0 0 10px #ff4d9d; }

@keyframes helix-spin {
  to { transform: rotateY(360deg); }
}

Sass source 57 lines

$rungs: 14;
$period: 4s;

.d-helix {
  display: grid;
  gap: 9px;
  transform-style: preserve-3d;
  transform: rotateZ(-14deg);

  &__rung {
    position: relative;
    width: 96px;
    height: 3px;
    border-radius: 3px;
    background: linear-gradient(90deg, var(--accent-2), transparent 35% 65%, var(--hot));
    transform-style: preserve-3d;
    animation: d-helix-spin $period linear infinite;
    animation-delay: var(--delay);

    // Negative delays start every rung part-way through the turn → the twist.
    @for $i from 1 through $rungs {
      &:nth-child(#{$i}) {
        --delay: #{$i * -0.22s};
      }
    }
  }

  i {
    position: absolute;
    top: -5px;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    background: var(--accent-2);
    box-shadow: 0 0 10px currentColor;
    color: var(--accent-2);
    // Billboard: spin the opposite way so the dot always faces the camera.
    animation: d-helix-spin $period linear infinite reverse;
    animation-delay: var(--delay);

    &:first-child {
      left: -6px;
    }

    &:last-child {
      right: -6px;
      background: var(--hot);
      color: var(--hot);
    }
  }
}

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