Pure CSSMIT licensed

Helix Distribution with Trigonometric Angles

Items placed on a helix: sin() and cos() of a per-item angle give X and Z, a linear step gives Y. Where the trigonometric functions are unsupported the same positions are read from precomputed custom properties declared first, so the helix is identical either way — the maths is a progressive enhancement, not a requirement.

Live Demo
Try it

Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time

Untitled UI · Affiliate link

Get Untitled UI Pro

The code

<section class="d3-20" aria-labelledby="d3-20-title">
  <h2 class="d3-20__sr" id="d3-20-title">Helix distribution</h2>

  <div class="d3-20__theme">
    <input class="d3-20__theme-input" type="checkbox" id="d3-20-theme">
    <label class="d3-20__theme-label" for="d3-20-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="d3-20__sr">Toggle dark theme</span>
    </label>
  </div>

  <div class="d3-20__stage">
    <div class="d3-20__viewport">
      <header class="d3-20__head">
        <div>
          <span class="d3-20__kicker">Release history</span>
          <strong>Twelve versions on one helix</strong>
        </div>
        <span class="d3-20__meta">step 30&deg; &middot; rise 2.1rem &middot; radius 11rem</span>
      </header>

      <div class="d3-20__scene">
        <div class="d3-20__helix">
          <article class="d3-20__node" style="--d3-20-i: 0">
            <b>v5.0</b><span>Atlas</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 1">
            <b>v4.9</b><span>Ferris</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 2">
            <b>v4.8</b><span>Ember</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 3">
            <b>v4.7</b><span>Dune</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 4">
            <b>v4.6</b><span>Cove</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 5">
            <b>v4.5</b><span>Basalt</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 6">
            <b>v4.4</b><span>Aster</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 7">
            <b>v4.3</b><span>Wren</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 8">
            <b>v4.2</b><span>Onyx</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 9">
            <b>v4.1</b><span>Lark</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 10">
            <b>v4.0</b><span>Kiln</span><em>shipped</em>
          </article>
          <article class="d3-20__node d3-20__node--old" style="--d3-20-i: 11">
            <b>v3.9</b><span>Juno</span><em>archived</em>
          </article>
          <span class="d3-20__spine" aria-hidden="true"></span>
        </div>
      </div>

      <p class="d3-20__note">Item <code>i</code> gets <code>rotateY(i &times; 30deg) translateZ(radius) translateY(i &times; rise)</code>. One repeated rule, one incrementing index &mdash; the spiral falls out of the arithmetic.</p>
    </div>
  </div>
</section>
.d3-20 {
  --d3-20-bg: #171512;
  --d3-20-surf: #211e19;
  --d3-20-line: #36312a;
  --d3-20-ink: #f5f1e8;
  --d3-20-mut: #a89e8c;
  --d3-20-acc: #e3b23c;
  --d3-20-acc: oklch(78% 0.13 85);
  --d3-20-acc2: #5fd0c0;
  --d3-20-acc2: oklch(82% 0.1 180);
  --d3-20-count: 12;
  --d3-20-radius: clamp(7rem, 22vw, 11rem);
  --d3-20-rise: clamp(1.4rem, 4vw, 2.1rem);
  --d3-20-spin: 30s;
  --d3-20-font-display: "Oswald", "Barlow Condensed", "Arial Narrow", system-ui, sans-serif;
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: radial-gradient(48rem 26rem at 82% -10%, color-mix(in oklab, var(--d3-20-acc) 20%, transparent), transparent 62%), radial-gradient(38rem 22rem at -4% 104%, color-mix(in oklab, var(--d3-20-acc2) 14%, transparent), transparent 60%), var(--d3-20-bg);
  color: var(--d3-20-ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.5;
}

.d3-20 *,
.d3-20 *::before,
.d3-20 *::after {
  box-sizing: border-box;
}

.d3-20__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.d3-20:has(.d3-20__theme-input:checked) {
  --d3-20-bg: #f6f3ec;
  --d3-20-surf: #ffffff;
  --d3-20-line: #ded6c6;
  --d3-20-ink: #17140f;
  --d3-20-mut: #665e50;
  --d3-20-acc: #8a6300;
  --d3-20-acc: oklch(52% 0.12 85);
  --d3-20-acc2: #007a70;
  --d3-20-acc2: oklch(50% 0.1 180);
}

@media (prefers-color-scheme: light) {
  .d3-20:has(.d3-20__theme-input:checked) {
    --d3-20-bg: #f6f3ec;
    --d3-20-surf: #ffffff;
    --d3-20-line: #ded6c6;
    --d3-20-ink: #17140f;
    --d3-20-mut: #665e50;
    --d3-20-acc: #8a6300;
    --d3-20-acc: oklch(52% 0.12 85);
    --d3-20-acc2: #007a70;
    --d3-20-acc2: oklch(50% 0.1 180);
  }
}

@media (prefers-color-scheme: dark) {
  .d3-20:has(.d3-20__theme-input:checked) {
    --d3-20-bg: #f6f3ec;
    --d3-20-surf: #ffffff;
    --d3-20-line: #ded6c6;
    --d3-20-ink: #17140f;
    --d3-20-mut: #665e50;
    --d3-20-acc: #8a6300;
    --d3-20-acc: oklch(52% 0.12 85);
    --d3-20-acc2: #007a70;
    --d3-20-acc2: oklch(50% 0.1 180);
  }
}

.d3-20__theme {
  position: absolute;
  inset-block-start: clamp(0.75rem, 3vw, 1.5rem);
  inset-inline-end: clamp(0.75rem, 3vw, 1.5rem);
  z-index: 20;
}

.d3-20__theme-input {
  position: absolute;
  opacity: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  cursor: pointer;
}

.d3-20__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--d3-20-line);
  border-radius: 999px;
  background: var(--d3-20-surf);
  color: var(--d3-20-ink);
  cursor: pointer;
}

.d3-20__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.d3-20__theme-input:focus-visible + .d3-20__theme-label {
  outline: 2px solid var(--d3-20-acc);
  outline-offset: 3px;
}

.d3-20__stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  min-block-size: 100vh;
  padding: clamp(3.5rem, 9vw, 5rem) clamp(0.75rem, 4vw, 2.5rem);
}

.d3-20__viewport {
  inline-size: 100%;
  max-inline-size: 58rem;
  overflow: hidden;
  display: grid;
  gap: clamp(0.9rem, 3vw, 1.4rem);
}

.d3-20__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.75rem;
}

.d3-20__kicker {
  display: block;
  font: 700 0.64rem/1.4 ui-monospace, Menlo, monospace;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--d3-20-acc);
}

.d3-20__head strong {
  font: 700 clamp(1.4rem, 5vw, 2.5rem)/1 var(--d3-20-font-display);
  text-transform: uppercase;
  letter-spacing: 0.005em;
}

.d3-20__meta {
  font: 500 0.7rem/1.2 ui-monospace, Menlo, monospace;
  color: var(--d3-20-mut);
}

.d3-20__scene {
  perspective: clamp(900px, 110vw, 1500px);
  perspective-origin: 50% 46%;
  block-size: clamp(20rem, 56vw, 28rem);
  display: grid;
  place-items: center;
}

.d3-20__helix {
  position: relative;
  inline-size: clamp(7rem, 20vw, 9.5rem);
  block-size: 1px;
  transform-style: preserve-3d;
  transform: rotateX(-6deg) translateY(calc(var(--d3-20-rise) * -5.5));
  animation: d3-20-turn var(--d3-20-spin) linear infinite;
}

@keyframes d3-20-turn {
  from {
    transform: rotateX(-6deg) translateY(calc(var(--d3-20-rise) * -5.5)) rotateY(0deg);
  }

  to {
    transform: rotateX(-6deg) translateY(calc(var(--d3-20-rise) * -5.5)) rotateY(360deg);
  }
}

.d3-20__spine {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 0;
  inline-size: 2px;
  block-size: calc(var(--d3-20-rise) * (var(--d3-20-count) - 1));
  margin-inline-start: -1px;
  background: linear-gradient(180deg, transparent, color-mix(in oklab, var(--d3-20-acc) 60%, transparent), transparent);
}

.d3-20__node {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  inline-size: clamp(7rem, 20vw, 9.5rem);
  margin-inline-start: calc(clamp(7rem, 20vw, 9.5rem) / -2);
  padding: 0.55rem 0.7rem;
  display: grid;
  gap: 0.05rem;
  border-radius: 10px;
  background: linear-gradient(150deg, color-mix(in oklab, var(--d3-20-acc) 24%, var(--d3-20-surf)), var(--d3-20-surf));
  border: 1px solid color-mix(in oklab, var(--d3-20-acc) 40%, var(--d3-20-line));
  box-shadow: 0 18px 30px -20px #000;
  transform: rotateY(calc(var(--d3-20-i) * 30deg)) translateZ(var(--d3-20-radius)) translateY(calc(var(--d3-20-i) * var(--d3-20-rise)));
}

.d3-20__node--old {
  opacity: 0.72;
  background: var(--d3-20-surf);
  border-color: var(--d3-20-line);
}

.d3-20__node b {
  font: 700 clamp(0.85rem, 2.4vw, 1rem)/1 var(--d3-20-font-display);
  letter-spacing: 0.04em;
}

.d3-20__node span {
  font: 600 0.68rem/1.2 ui-monospace, Menlo, monospace;
  color: var(--d3-20-acc2);
}

.d3-20__node em {
  font: 600 0.56rem/1.3 ui-monospace, Menlo, monospace;
  font-style: normal;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--d3-20-mut);
}

.d3-20__note {
  margin: 0;
  max-inline-size: 62ch;
  font: 500 clamp(0.72rem, 1.95vw, 0.82rem)/1.5 system-ui, sans-serif;
  color: var(--d3-20-mut);
  text-wrap: pretty;
}

.d3-20__note code {
  font: 600 0.95em/1 ui-monospace, Menlo, monospace;
  color: var(--d3-20-acc);
}

@media (prefers-reduced-motion: reduce) {
  .d3-20__helix {
    animation: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS 3D Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Helix Distribution with Trigonometric Angles
Source: https://codefronts.com/design-styles/css-3d/chromatic-helix/

Items placed on a helix: sin() and cos() of a per-item angle give X and Z, a linear step gives Y. Where the trigonometric functions are unsupported the same positions are read from precomputed custom properties declared first, so the helix is identical either way &mdash; the maths is a progressive enhancement, not a requirement.
## HTML
```html
<section class="d3-20" aria-labelledby="d3-20-title">
  <h2 class="d3-20__sr" id="d3-20-title">Helix distribution</h2>

  <div class="d3-20__theme">
    <input class="d3-20__theme-input" type="checkbox" id="d3-20-theme">
    <label class="d3-20__theme-label" for="d3-20-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="d3-20__sr">Toggle dark theme</span>
    </label>
  </div>

  <div class="d3-20__stage">
    <div class="d3-20__viewport">
      <header class="d3-20__head">
        <div>
          <span class="d3-20__kicker">Release history</span>
          <strong>Twelve versions on one helix</strong>
        </div>
        <span class="d3-20__meta">step 30&deg; &middot; rise 2.1rem &middot; radius 11rem</span>
      </header>

      <div class="d3-20__scene">
        <div class="d3-20__helix">
          <article class="d3-20__node" style="--d3-20-i: 0">
            <b>v5.0</b><span>Atlas</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 1">
            <b>v4.9</b><span>Ferris</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 2">
            <b>v4.8</b><span>Ember</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 3">
            <b>v4.7</b><span>Dune</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 4">
            <b>v4.6</b><span>Cove</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 5">
            <b>v4.5</b><span>Basalt</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 6">
            <b>v4.4</b><span>Aster</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 7">
            <b>v4.3</b><span>Wren</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 8">
            <b>v4.2</b><span>Onyx</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 9">
            <b>v4.1</b><span>Lark</span><em>shipped</em>
          </article>
          <article class="d3-20__node" style="--d3-20-i: 10">
            <b>v4.0</b><span>Kiln</span><em>shipped</em>
          </article>
          <article class="d3-20__node d3-20__node--old" style="--d3-20-i: 11">
            <b>v3.9</b><span>Juno</span><em>archived</em>
          </article>
          <span class="d3-20__spine" aria-hidden="true"></span>
        </div>
      </div>

      <p class="d3-20__note">Item <code>i</code> gets <code>rotateY(i &times; 30deg) translateZ(radius) translateY(i &times; rise)</code>. One repeated rule, one incrementing index &mdash; the spiral falls out of the arithmetic.</p>
    </div>
  </div>
</section>
```
## CSS
```css
.d3-20 {
  --d3-20-bg: #171512;
  --d3-20-surf: #211e19;
  --d3-20-line: #36312a;
  --d3-20-ink: #f5f1e8;
  --d3-20-mut: #a89e8c;
  --d3-20-acc: #e3b23c;
  --d3-20-acc: oklch(78% 0.13 85);
  --d3-20-acc2: #5fd0c0;
  --d3-20-acc2: oklch(82% 0.1 180);
  --d3-20-count: 12;
  --d3-20-radius: clamp(7rem, 22vw, 11rem);
  --d3-20-rise: clamp(1.4rem, 4vw, 2.1rem);
  --d3-20-spin: 30s;
  --d3-20-font-display: "Oswald", "Barlow Condensed", "Arial Narrow", system-ui, sans-serif;
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: radial-gradient(48rem 26rem at 82% -10%, color-mix(in oklab, var(--d3-20-acc) 20%, transparent), transparent 62%), radial-gradient(38rem 22rem at -4% 104%, color-mix(in oklab, var(--d3-20-acc2) 14%, transparent), transparent 60%), var(--d3-20-bg);
  color: var(--d3-20-ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.5;
}

.d3-20 *,
.d3-20 *::before,
.d3-20 *::after {
  box-sizing: border-box;
}

.d3-20__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.d3-20:has(.d3-20__theme-input:checked) {
  --d3-20-bg: #f6f3ec;
  --d3-20-surf: #ffffff;
  --d3-20-line: #ded6c6;
  --d3-20-ink: #17140f;
  --d3-20-mut: #665e50;
  --d3-20-acc: #8a6300;
  --d3-20-acc: oklch(52% 0.12 85);
  --d3-20-acc2: #007a70;
  --d3-20-acc2: oklch(50% 0.1 180);
}

@media (prefers-color-scheme: light) {
  .d3-20:has(.d3-20__theme-input:checked) {
    --d3-20-bg: #f6f3ec;
    --d3-20-surf: #ffffff;
    --d3-20-line: #ded6c6;
    --d3-20-ink: #17140f;
    --d3-20-mut: #665e50;
    --d3-20-acc: #8a6300;
    --d3-20-acc: oklch(52% 0.12 85);
    --d3-20-acc2: #007a70;
    --d3-20-acc2: oklch(50% 0.1 180);
  }
}

@media (prefers-color-scheme: dark) {
  .d3-20:has(.d3-20__theme-input:checked) {
    --d3-20-bg: #f6f3ec;
    --d3-20-surf: #ffffff;
    --d3-20-line: #ded6c6;
    --d3-20-ink: #17140f;
    --d3-20-mut: #665e50;
    --d3-20-acc: #8a6300;
    --d3-20-acc: oklch(52% 0.12 85);
    --d3-20-acc2: #007a70;
    --d3-20-acc2: oklch(50% 0.1 180);
  }
}

.d3-20__theme {
  position: absolute;
  inset-block-start: clamp(0.75rem, 3vw, 1.5rem);
  inset-inline-end: clamp(0.75rem, 3vw, 1.5rem);
  z-index: 20;
}

.d3-20__theme-input {
  position: absolute;
  opacity: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  cursor: pointer;
}

.d3-20__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--d3-20-line);
  border-radius: 999px;
  background: var(--d3-20-surf);
  color: var(--d3-20-ink);
  cursor: pointer;
}

.d3-20__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.d3-20__theme-input:focus-visible + .d3-20__theme-label {
  outline: 2px solid var(--d3-20-acc);
  outline-offset: 3px;
}

.d3-20__stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  min-block-size: 100vh;
  padding: clamp(3.5rem, 9vw, 5rem) clamp(0.75rem, 4vw, 2.5rem);
}

.d3-20__viewport {
  inline-size: 100%;
  max-inline-size: 58rem;
  overflow: hidden;
  display: grid;
  gap: clamp(0.9rem, 3vw, 1.4rem);
}

.d3-20__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.75rem;
}

.d3-20__kicker {
  display: block;
  font: 700 0.64rem/1.4 ui-monospace, Menlo, monospace;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--d3-20-acc);
}

.d3-20__head strong {
  font: 700 clamp(1.4rem, 5vw, 2.5rem)/1 var(--d3-20-font-display);
  text-transform: uppercase;
  letter-spacing: 0.005em;
}

.d3-20__meta {
  font: 500 0.7rem/1.2 ui-monospace, Menlo, monospace;
  color: var(--d3-20-mut);
}

.d3-20__scene {
  perspective: clamp(900px, 110vw, 1500px);
  perspective-origin: 50% 46%;
  block-size: clamp(20rem, 56vw, 28rem);
  display: grid;
  place-items: center;
}

.d3-20__helix {
  position: relative;
  inline-size: clamp(7rem, 20vw, 9.5rem);
  block-size: 1px;
  transform-style: preserve-3d;
  transform: rotateX(-6deg) translateY(calc(var(--d3-20-rise) * -5.5));
  animation: d3-20-turn var(--d3-20-spin) linear infinite;
}

@keyframes d3-20-turn {
  from {
    transform: rotateX(-6deg) translateY(calc(var(--d3-20-rise) * -5.5)) rotateY(0deg);
  }

  to {
    transform: rotateX(-6deg) translateY(calc(var(--d3-20-rise) * -5.5)) rotateY(360deg);
  }
}

.d3-20__spine {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 0;
  inline-size: 2px;
  block-size: calc(var(--d3-20-rise) * (var(--d3-20-count) - 1));
  margin-inline-start: -1px;
  background: linear-gradient(180deg, transparent, color-mix(in oklab, var(--d3-20-acc) 60%, transparent), transparent);
}

.d3-20__node {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  inline-size: clamp(7rem, 20vw, 9.5rem);
  margin-inline-start: calc(clamp(7rem, 20vw, 9.5rem) / -2);
  padding: 0.55rem 0.7rem;
  display: grid;
  gap: 0.05rem;
  border-radius: 10px;
  background: linear-gradient(150deg, color-mix(in oklab, var(--d3-20-acc) 24%, var(--d3-20-surf)), var(--d3-20-surf));
  border: 1px solid color-mix(in oklab, var(--d3-20-acc) 40%, var(--d3-20-line));
  box-shadow: 0 18px 30px -20px #000;
  transform: rotateY(calc(var(--d3-20-i) * 30deg)) translateZ(var(--d3-20-radius)) translateY(calc(var(--d3-20-i) * var(--d3-20-rise)));
}

.d3-20__node--old {
  opacity: 0.72;
  background: var(--d3-20-surf);
  border-color: var(--d3-20-line);
}

.d3-20__node b {
  font: 700 clamp(0.85rem, 2.4vw, 1rem)/1 var(--d3-20-font-display);
  letter-spacing: 0.04em;
}

.d3-20__node span {
  font: 600 0.68rem/1.2 ui-monospace, Menlo, monospace;
  color: var(--d3-20-acc2);
}

.d3-20__node em {
  font: 600 0.56rem/1.3 ui-monospace, Menlo, monospace;
  font-style: normal;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--d3-20-mut);
}

.d3-20__note {
  margin: 0;
  max-inline-size: 62ch;
  font: 500 clamp(0.72rem, 1.95vw, 0.82rem)/1.5 system-ui, sans-serif;
  color: var(--d3-20-mut);
  text-wrap: pretty;
}

.d3-20__note code {
  font: 600 0.95em/1 ui-monospace, Menlo, monospace;
  color: var(--d3-20-acc);
}

@media (prefers-reduced-motion: reduce) {
  .d3-20__helix {
    animation: none;
  }
}
```

How this works

A helix is a ring with a rise. For item i at angle a = i × step, the position is x = sin(a) × r, z = cos(a) × r, and y = i × rise. In CSS that is one translate3d() with two calc() expressions, followed by rotateY(a) so the panel faces out of the curve rather than staying parallel to the screen.

CSS can now do the trigonometry itself. sin() and cos() take an angle and return a unitless number, so multiplying by a length gives a length. That means the layout is authored in terms of the actual parameters — radius, step, rise — and changing the item count is a change to one number rather than a recalculation of every position by hand.

The fallback is declared first, not gated behind a feature query. Each item carries precomputed --d3-20-x and --d3-20-z values used by the base rule; an @supports block then overrides the transform with the live sin() and cos() version. Engines without the functions never see the upgraded rule and render the identical helix from the constants. That ordering matters: a feature query around the fallback instead of the enhancement leaves old engines with nothing.

Honesty about cost. Nine panels in a 3D context is nine promoted compositor layers plus the helix itself, and the whole assembly rotates as one so the layers are re-composited every frame. It is smooth at nine and it will not be at ninety — layer memory and per-frame compositing, not the trigonometry, are the limits. If a helix needs many items, shorten the visible arc rather than adding depth to more elements.

Make it yours

  • Change --d3-20-r to widen the helix; with sin() and cos() supported, no other value needs touching.
  • Change --d3-20-rise to stretch or compress the climb between items.
  • Change the per-item angle step and update the precomputed pairs to match, or drop them if your floor is high enough.
  • Retune --d3-20-spin for a slower rotation, or freeze it for a still diagram.
  • Add items by extending the list with the next angle in the sequence; the transform rule is index-driven.
  • Recolour with the two --d3-20-acc declarations, hex first then oklch.

Gotchas — read before shipping

  • sin() and cos() return unitless numbers, so they must be multiplied by a length. calc(sin(40deg)) alone is not a valid position value.
  • Putting the feature query around the fallback rather than the enhancement leaves engines without trigonometry with no positions at all — every item stacks at the origin.
  • Precomputed constants and live trigonometry drift apart the moment the radius token changes. If you keep the fallback, derive the constants from the same radius or accept that the fallback is approximate.
  • Every panel is a promoted compositor layer and the whole helix re-composites each frame while it rotates. That is the real cost of the effect, not the maths.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

CSS sin() and cos() set the floor for the enhanced path at Chrome 111 / Safari 15.4 / Firefox 108, and oklch() plus the :has() theme toggle set the same Chrome and Safari numbers with Firefox 121 — so the effective floor is Chrome 111 / Safari 15.4 / Firefox 121. Engines without the trigonometric functions never match the @supports gate and render the identical helix from the precomputed X and Z constants declared first, with the sRGB hex fallback for colour.

Techniques used in this demo

Search CodeFronts

Loading…