25 CSS Spinners 10 / 25

DNA Double Helix Spinner

Eight nucleotide pairs animate as two interweaving dot chains — one cyan, one pink — translating horizontally in opposing directions to simulate a rotating DNA double helix.

Pure CSS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="sp-10">
  <div class="sp-10__dna">
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
    <div class="sp-10__pair"><div class="sp-10__dot-a"></div><div class="sp-10__bridge"></div><div class="sp-10__dot-b"></div></div>
  </div>
</div>
.sp-10,.sp-10 *,.sp-10 *::before,.sp-10 *::after{box-sizing:border-box;margin:0;padding:0}
.sp-10{
  --bg:#040d1a;
  --ca:#00bcd4;
  --cb:#f06292;
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:100vh;
  background:var(--bg);
}
.sp-10__dna{
  display:flex;
  flex-direction:column;
  gap:0;
  align-items:center;
}
.sp-10__pair{
  display:flex;
  align-items:center;
  gap:0;
  height:9px;
}
.sp-10__dot-a,.sp-10__dot-b{
  width:10px;
  height:10px;
  border-radius:50%;
}
.sp-10__dot-a{
  background:var(--ca);
  box-shadow:0 0 6px var(--ca);
}
.sp-10__dot-b{
  background:var(--cb);
  box-shadow:0 0 6px var(--cb);
}
.sp-10__bridge{
  width:28px;
  height:1px;
  background:linear-gradient(to right,var(--ca),var(--cb));
  /* The bridge represents the base-pair connection between the two
     strands. As the strands cross (50% of the cycle, dots meeting at
     center), the visible distance between them shrinks to zero —
     so the bridge should ALSO shrink to invisible at that moment.
     A symmetric scaleX keyframe makes the bridge "breathe" between
     full-width (when strands are spread) and zero-width (when they
     cross). transform-origin:center keeps the bridge centered as it
     scales. */
  transform-origin:center;
  animation:sp-10-bridge 1.4s ease-in-out infinite;
}
.sp-10__pair:nth-child(1) .sp-10__bridge{animation-delay:calc(0 * 0.14s)}
.sp-10__pair:nth-child(2) .sp-10__bridge{animation-delay:calc(1 * 0.14s)}
.sp-10__pair:nth-child(3) .sp-10__bridge{animation-delay:calc(2 * 0.14s)}
.sp-10__pair:nth-child(4) .sp-10__bridge{animation-delay:calc(3 * 0.14s)}
.sp-10__pair:nth-child(5) .sp-10__bridge{animation-delay:calc(4 * 0.14s)}
.sp-10__pair:nth-child(6) .sp-10__bridge{animation-delay:calc(5 * 0.14s)}
.sp-10__pair:nth-child(7) .sp-10__bridge{animation-delay:calc(6 * 0.14s)}
.sp-10__pair:nth-child(8) .sp-10__bridge{animation-delay:calc(7 * 0.14s)}
@keyframes sp-10-bridge{
  /* 0%/100% = strands at extremes → bridge full width visible. */
  0%,100%{transform:scaleX(1);opacity:0.4}
  /* 50% = strands crossing at center → bridge collapsed + invisible. */
  50%{transform:scaleX(0.05);opacity:0}
}
/* Two strands run the SAME keyframe but at OPPOSITE PHASE so they
   cross each other. The trick: dot-A's keyframe goes right→left→right,
   dot-B's keyframe goes left→right→left. With opposite phase, when
   strand A is pushed +16, strand B is pushed -16 — they meet and cross
   at the center every half-cycle, producing the X-shape that reads as
   a helix projection.

   Why not animation-direction:reverse? Because the keyframe is symmetric
   (0% and 100% are identical), so reverse plays the SAME visual sequence.
   Reverse only flips things visually if 0% ≠ 100%. Two distinct keyframes
   (helix-a and helix-b that start at opposite x positions) is the
   reliable approach.

   The per-pair animation-delay staggers the crossing motion vertically
   so it propagates downward in a wave (~0.14s per row). */
.sp-10__pair:nth-child(1) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(0 * 0.14s)}
.sp-10__pair:nth-child(2) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(1 * 0.14s)}
.sp-10__pair:nth-child(3) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(2 * 0.14s)}
.sp-10__pair:nth-child(4) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(3 * 0.14s)}
.sp-10__pair:nth-child(5) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(4 * 0.14s)}
.sp-10__pair:nth-child(6) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(5 * 0.14s)}
.sp-10__pair:nth-child(7) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(6 * 0.14s)}
.sp-10__pair:nth-child(8) .sp-10__dot-a{animation:sp-10-helix-a 1.4s ease-in-out infinite;animation-delay:calc(7 * 0.14s)}
.sp-10__pair:nth-child(1) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(0 * 0.14s)}
.sp-10__pair:nth-child(2) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(1 * 0.14s)}
.sp-10__pair:nth-child(3) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(2 * 0.14s)}
.sp-10__pair:nth-child(4) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(3 * 0.14s)}
.sp-10__pair:nth-child(5) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(4 * 0.14s)}
.sp-10__pair:nth-child(6) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(5 * 0.14s)}
.sp-10__pair:nth-child(7) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(6 * 0.14s)}
.sp-10__pair:nth-child(8) .sp-10__dot-b{animation:sp-10-helix-b 1.4s ease-in-out infinite;animation-delay:calc(7 * 0.14s)}
/* Strand A starts pushed RIGHT then swings LEFT (front of the helix at 50%). */
@keyframes sp-10-helix-a{
  0%,100%{transform:translateX(16px);opacity:0.2}
  50%{transform:translateX(-16px);opacity:1}
}
/* Strand B starts pushed LEFT then swings RIGHT — opposite phase, so
   strands A and B cross at the center every half-cycle (the X pattern). */
@keyframes sp-10-helix-b{
  0%,100%{transform:translateX(-16px);opacity:1}
  50%{transform:translateX(16px);opacity:0.2}
}
@media (prefers-reduced-motion: reduce){
  .sp-10__dot-a,.sp-10__dot-b{animation:none;transform:none}
}

How this works

Each row is a flex container holding a cyan dot, a faint bridge line, and a pink dot. The cyan dots all share sp-10-helix which translates them from translateX(16px) to translateX(-16px) and back, while the pink dots use animation-direction:reverse on the same keyframe so they mirror the motion exactly — when cyan dots are pushed right, pink dots are pushed left, creating the characteristic X-crossing pattern of a helix projection.

Row-level animation-delay values stagger each pair by 0.14s so the crossing motion propagates downward in a wave. The opacity cycling from 1 to 0.2 simulates depth — dots in the "back" of the helix appear dimmer.

Customize

  • Change helix colours via --ca (strand A) and --cb (strand B) — a gold/silver combination creates a metallic look.
  • Add more base pairs by duplicating .sp-10__pair rows and extending the nth-child delay pattern by 0.14s increments.
  • Increase bridge width from 28px to 40px and translate distance from 16px to 22px for a wider helix appearance.
  • Change the animation duration from 1.4s to 2s for a slower, more molecular animation suitable for science-themed UIs.
  • Replace dots with short rectangles (border-radius:2px on a 10px × 4px element) to simulate nucleotide base-pair segments.

Watch out for

  • The animation-direction:reverse override must use !important here because the delay rules have higher specificity — verify overrides are working in browser DevTools if helix appears to move in the same direction on both strands.
  • The translateX values are symmetric around zero — changing only one strand's translation distance will break the crossing visual; always update both.
  • On very small screens the 108px-wide helix (8 × 9px gaps + dots) may overflow narrow containers — test at 320px viewport width.

Browser support

ChromeSafariFirefoxEdge
60+ 12+ 60+ 60+

Uses only flexbox and transform/opacity animations; no modern-only features required.

Search CodeFronts

Loading…