14 CSS Wave Section Dividers12 / 14

Pure CSSMIT licensed

CSS animated sine wave section divider transition

A mathematically correct sine wave — sampled from y = A·sin(x) rather than eyeballed with bezier handles — animated as a travelling waveform. The geometric, signal-processing look that tech, audio and web3 landing pages keep asking for.

Published

Live Demo
Try it

The code

<div class="wsd-12-group">
<section class="wsd-12a">
  <div class="wsd-12a__top">
    <p class="wsd-12a__eyebrow">y = A · sin(x)</p>
    <h2 class="wsd-12a__title">Signal, not decoration</h2>
    <div class="wsd-12a__waves" aria-hidden="true">
      <div class="wsd-12a__track wsd-12a__track--b">
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,70 C90,40 270,40 360,70 C450,100 630,100 720,70 C810,40 990,40 1080,70 C1170,100 1350,100 1440,70 L1440,120 L0,120 Z"></path></svg>
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,70 C90,40 270,40 360,70 C450,100 630,100 720,70 C810,40 990,40 1080,70 C1170,100 1350,100 1440,70 L1440,120 L0,120 Z"></path></svg>
      </div>
      <div class="wsd-12a__track wsd-12a__track--a">
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,80 C60,50 180,50 240,80 C300,110 420,110 480,80 C540,50 660,50 720,80 C780,110 900,110 960,80 C1020,50 1140,50 1200,80 C1260,110 1380,110 1440,80 L1440,120 L0,120 Z"></path></svg>
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,80 C60,50 180,50 240,80 C300,110 420,110 480,80 C540,50 660,50 720,80 C780,110 900,110 960,80 C1020,50 1140,50 1200,80 C1260,110 1380,110 1440,80 L1440,120 L0,120 Z"></path></svg>
      </div>
    </div>
  </div>
  <div class="wsd-12a__bottom"><p class="wsd-12a__copy">Each half-period is identical, so the loop is mathematically seamless.</p></div>
</section>
<section class="wsd-12b">
  <div class="wsd-12b__top">
    <h2 class="wsd-12b__title">Stroked waveform seam</h2>
    <div class="wsd-12b__waves" aria-hidden="true">
      <div class="wsd-12b__track">
        <svg viewBox="0 0 1440 100" preserveAspectRatio="none" focusable="false"><path class="wsd-12b__line" d="M0,50 C60,14 180,14 240,50 C300,86 420,86 480,50 C540,14 660,14 720,50 C780,86 900,86 960,50 C1020,14 1140,14 1200,50 C1260,86 1380,86 1440,50" fill="none"></path></svg>
        <svg viewBox="0 0 1440 100" preserveAspectRatio="none" focusable="false"><path class="wsd-12b__line" d="M0,50 C60,14 180,14 240,50 C300,86 420,86 480,50 C540,14 660,14 720,50 C780,86 900,86 960,50 C1020,14 1140,14 1200,50 C1260,86 1380,86 1440,50" fill="none"></path></svg>
      </div>
    </div>
  </div>
  <div class="wsd-12b__bottom"><p class="wsd-12b__copy">A stroke instead of a fill turns the divider into an instrument readout.</p></div>
</section>
</div>
.wsd-12-group {
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.wsd-12-group > section {
  width: 100%;
}

.wsd-12a,
.wsd-12a *,
.wsd-12a *::before,
.wsd-12a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.wsd-12a {
  --void: oklch(0.15 0.03 265);
  --signal: oklch(0.78 0.18 165);
  --floor: oklch(0.95 0.02 165);
  font-family: ui-monospace,'Segoe UI',monospace;
}

.wsd-12a__top {
  position: relative;
  overflow: hidden;
  background: var(--void);
  color: #fff;
  padding: 70px 24px 124px;
  text-align: center;
}

.wsd-12a__eyebrow {
  font-size: 12px;
  letter-spacing: .2em;
  color: var(--signal);
  margin-bottom: 12px;
}

.wsd-12a__title {
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-size: clamp(25px,4vw,40px);
}

.wsd-12a__waves {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: clamp(64px,10vw,120px);
}

.wsd-12a__track {
  position: absolute;
  inset: 0;
  display: flex;
  width: 200%;
}

.wsd-12a__track svg {
  display: block;
  width: 50%;
  height: 100%;
}

.wsd-12a__track--b {
  animation: wsd-12a-run 18s linear infinite;
}

.wsd-12a__track--b path {
  fill: color-mix(in oklab,var(--signal) 34%,transparent);
}

.wsd-12a__track--a {
  animation: wsd-12a-run 11s linear infinite;
}

.wsd-12a__track--a path {
  fill: var(--floor);
}

.wsd-12a__bottom {
  background: var(--floor);
  color: oklch(0.28 0.05 165);
  padding: 30px 24px 58px;
  text-align: center;
}

.wsd-12a__copy {
  font-size: 14px;
  opacity: .76;
}

@keyframes wsd-12a-run {
  from {
    transform: translate3d(0,0,0);
  }

  to {
    transform: translate3d(-50%,0,0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wsd-12a__track {
    animation: none!important;
  }
}

.wsd-12b,
.wsd-12b *,
.wsd-12b *::before,
.wsd-12b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.wsd-12b {
  --void: oklch(0.13 0.02 150);
  --trace: oklch(0.85 0.19 145);
  font-family: ui-monospace,'Segoe UI',monospace;
}

.wsd-12b__top {
  position: relative;
  overflow: hidden;
  background: var(--void);
  color: #eafff2;
  padding: 66px 24px 96px;
  text-align: center;
}

.wsd-12b__title {
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-size: clamp(23px,3.6vw,34px);
}

.wsd-12b__waves {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: clamp(56px,8vw,100px);
}

.wsd-12b__track {
  position: absolute;
  inset: 0;
  display: flex;
  width: 200%;
  animation: wsd-12b-run 8s linear infinite;
}

.wsd-12b__track svg {
  display: block;
  width: 50%;
  height: 100%;
  overflow: visible;
}

.wsd-12b__line {
  stroke: var(--trace);
  stroke-width: 3;
  vector-effect: non-scaling-stroke;
  filter: drop-shadow(0 0 6px color-mix(in oklab,var(--trace) 70%,transparent));
}

.wsd-12b__bottom {
  background: oklch(0.2 0.03 150);
  color: #dcf7e8;
  padding: 26px 24px 56px;
  text-align: center;
  border-top: 1px solid color-mix(in oklab,var(--trace) 35%,transparent);
}

.wsd-12b__copy {
  font-size: 14px;
  opacity: .8;
}

@keyframes wsd-12b-run {
  from {
    transform: translate3d(0,0,0);
  }

  to {
    transform: translate3d(-50%,0,0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wsd-12b__track {
    animation: none;
  }
}
/* No JavaScript — sampled sine paths (control points at 1/4 and 3/4 of each half-period) tiled twice and translated -50% forever. */

/* No JavaScript — vector-effect:non-scaling-stroke keeps the trace 3px thick even though the SVG is stretched horizontally. */
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 Wave Section Divider 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: CSS animated sine wave section divider transition
Source: https://codefronts.com/layouts/css-wave-section-dividers/css-animated-sine-wave-section-divider-transition/

A mathematically correct sine wave — sampled from y = A·sin(x) rather than eyeballed with bezier handles — animated as a travelling waveform. The geometric, signal-processing look that tech, audio and web3 landing pages keep asking for.
## HTML
```html
<div class="wsd-12-group">
<section class="wsd-12a">
  <div class="wsd-12a__top">
    <p class="wsd-12a__eyebrow">y = A · sin(x)</p>
    <h2 class="wsd-12a__title">Signal, not decoration</h2>
    <div class="wsd-12a__waves" aria-hidden="true">
      <div class="wsd-12a__track wsd-12a__track--b">
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,70 C90,40 270,40 360,70 C450,100 630,100 720,70 C810,40 990,40 1080,70 C1170,100 1350,100 1440,70 L1440,120 L0,120 Z"></path></svg>
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,70 C90,40 270,40 360,70 C450,100 630,100 720,70 C810,40 990,40 1080,70 C1170,100 1350,100 1440,70 L1440,120 L0,120 Z"></path></svg>
      </div>
      <div class="wsd-12a__track wsd-12a__track--a">
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,80 C60,50 180,50 240,80 C300,110 420,110 480,80 C540,50 660,50 720,80 C780,110 900,110 960,80 C1020,50 1140,50 1200,80 C1260,110 1380,110 1440,80 L1440,120 L0,120 Z"></path></svg>
        <svg viewBox="0 0 1440 120" preserveAspectRatio="none" focusable="false"><path d="M0,80 C60,50 180,50 240,80 C300,110 420,110 480,80 C540,50 660,50 720,80 C780,110 900,110 960,80 C1020,50 1140,50 1200,80 C1260,110 1380,110 1440,80 L1440,120 L0,120 Z"></path></svg>
      </div>
    </div>
  </div>
  <div class="wsd-12a__bottom"><p class="wsd-12a__copy">Each half-period is identical, so the loop is mathematically seamless.</p></div>
</section>
<section class="wsd-12b">
  <div class="wsd-12b__top">
    <h2 class="wsd-12b__title">Stroked waveform seam</h2>
    <div class="wsd-12b__waves" aria-hidden="true">
      <div class="wsd-12b__track">
        <svg viewBox="0 0 1440 100" preserveAspectRatio="none" focusable="false"><path class="wsd-12b__line" d="M0,50 C60,14 180,14 240,50 C300,86 420,86 480,50 C540,14 660,14 720,50 C780,86 900,86 960,50 C1020,14 1140,14 1200,50 C1260,86 1380,86 1440,50" fill="none"></path></svg>
        <svg viewBox="0 0 1440 100" preserveAspectRatio="none" focusable="false"><path class="wsd-12b__line" d="M0,50 C60,14 180,14 240,50 C300,86 420,86 480,50 C540,14 660,14 720,50 C780,86 900,86 960,50 C1020,14 1140,14 1200,50 C1260,86 1380,86 1440,50" fill="none"></path></svg>
      </div>
    </div>
  </div>
  <div class="wsd-12b__bottom"><p class="wsd-12b__copy">A stroke instead of a fill turns the divider into an instrument readout.</p></div>
</section>
</div>
```
## CSS
```css
.wsd-12-group {
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.wsd-12-group > section {
  width: 100%;
}

.wsd-12a,
.wsd-12a *,
.wsd-12a *::before,
.wsd-12a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.wsd-12a {
  --void: oklch(0.15 0.03 265);
  --signal: oklch(0.78 0.18 165);
  --floor: oklch(0.95 0.02 165);
  font-family: ui-monospace,'Segoe UI',monospace;
}

.wsd-12a__top {
  position: relative;
  overflow: hidden;
  background: var(--void);
  color: #fff;
  padding: 70px 24px 124px;
  text-align: center;
}

.wsd-12a__eyebrow {
  font-size: 12px;
  letter-spacing: .2em;
  color: var(--signal);
  margin-bottom: 12px;
}

.wsd-12a__title {
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-size: clamp(25px,4vw,40px);
}

.wsd-12a__waves {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: clamp(64px,10vw,120px);
}

.wsd-12a__track {
  position: absolute;
  inset: 0;
  display: flex;
  width: 200%;
}

.wsd-12a__track svg {
  display: block;
  width: 50%;
  height: 100%;
}

.wsd-12a__track--b {
  animation: wsd-12a-run 18s linear infinite;
}

.wsd-12a__track--b path {
  fill: color-mix(in oklab,var(--signal) 34%,transparent);
}

.wsd-12a__track--a {
  animation: wsd-12a-run 11s linear infinite;
}

.wsd-12a__track--a path {
  fill: var(--floor);
}

.wsd-12a__bottom {
  background: var(--floor);
  color: oklch(0.28 0.05 165);
  padding: 30px 24px 58px;
  text-align: center;
}

.wsd-12a__copy {
  font-size: 14px;
  opacity: .76;
}

@keyframes wsd-12a-run {
  from {
    transform: translate3d(0,0,0);
  }

  to {
    transform: translate3d(-50%,0,0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wsd-12a__track {
    animation: none!important;
  }
}

.wsd-12b,
.wsd-12b *,
.wsd-12b *::before,
.wsd-12b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.wsd-12b {
  --void: oklch(0.13 0.02 150);
  --trace: oklch(0.85 0.19 145);
  font-family: ui-monospace,'Segoe UI',monospace;
}

.wsd-12b__top {
  position: relative;
  overflow: hidden;
  background: var(--void);
  color: #eafff2;
  padding: 66px 24px 96px;
  text-align: center;
}

.wsd-12b__title {
  font-family: 'Segoe UI',system-ui,sans-serif;
  font-size: clamp(23px,3.6vw,34px);
}

.wsd-12b__waves {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: clamp(56px,8vw,100px);
}

.wsd-12b__track {
  position: absolute;
  inset: 0;
  display: flex;
  width: 200%;
  animation: wsd-12b-run 8s linear infinite;
}

.wsd-12b__track svg {
  display: block;
  width: 50%;
  height: 100%;
  overflow: visible;
}

.wsd-12b__line {
  stroke: var(--trace);
  stroke-width: 3;
  vector-effect: non-scaling-stroke;
  filter: drop-shadow(0 0 6px color-mix(in oklab,var(--trace) 70%,transparent));
}

.wsd-12b__bottom {
  background: oklch(0.2 0.03 150);
  color: #dcf7e8;
  padding: 26px 24px 56px;
  text-align: center;
  border-top: 1px solid color-mix(in oklab,var(--trace) 35%,transparent);
}

.wsd-12b__copy {
  font-size: 14px;
  opacity: .8;
}

@keyframes wsd-12b-run {
  from {
    transform: translate3d(0,0,0);
  }

  to {
    transform: translate3d(-50%,0,0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wsd-12b__track {
    animation: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — sampled sine paths (control points at 1/4 and 3/4 of each half-period) tiled twice and translated -50% forever. */

/* No JavaScript — vector-effect:non-scaling-stroke keeps the trace 3px thick even though the SVG is stretched horizontally. */
```

How this works

A true sine is built by sampling the function at regular intervals and emitting one cubic segment per half-period; because every half-period is identical, the path tiles perfectly, which is exactly what a seamless scroll animation needs.

<!-- one period = 360 units, amplitude 30 -->
M0,60 C45,30 135,30 180,60 C225,90 315,90 360,60 …

Two copies travel left at different speeds and amplitudes; where they cross, the interference pattern looks like a real waveform rather than a repeating decal. A second layer at 0.66× speed and 60% opacity is enough to break the loop's perceptibility.

Motion is transform: translate3d() only — the divider costs one composited layer and no main-thread work, so INP stays clean even during hydration.

Make it yours

  • Amplitude: change the 30/90 control Y values in pairs to keep it a true sine.
  • Frequency: shorten the period (360 → 240) for a denser signal.
  • Add a stroked (unfilled) sine on top for an oscilloscope trace.
  • Tint layers with color-mix for a spectrum feel.

Gotchas — read before shipping

  • Bezier-approximated sines drift if control points are not at 1/4 and 3/4 of each half-period.
  • Keep the period a clean divisor of the viewBox width or tiling breaks.
  • One infinite animation per divider — stacking five is a battery drain on mobile.
  • Reduced motion must freeze it; a scrolling waveform is a strong vestibular trigger.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

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

Keyframed transforms on SVG — universal. Reduced motion falls back to a static waveform.

Techniques used in this demo

Search CodeFronts

Loading…