svg · css

Section dividers that stretch

Building a wave divider from the SVG export: inline markup or a CSS background, preserveAspectRatio, matching the fill, stacking layers and flipping.

Published September 18, 2026

A section divider marks where one band of a page ends and the next begins. A border does that in a pixel. A wave does it with a shape, and the whole job is carrying the color of one section into the other.

The wave generator draws that shape on a 1440-unit canvas at a height of 160, 240, 320 or 480. What you do with the export is the interesting part, because a divider has to work at every viewport width without ever looking stretched in a way the reader notices.

What comes out of the export

Set five points, a mid amplitude, and a blue fill, and the SVG looks like this:

<svg viewBox="0 0 1440 320" preserveAspectRatio="none" aria-hidden="true">
  <path
    fill="#1e8fe5"
    d="M0,160 Q180,64 360,128 Q540,192 720,112 Q900,32 1080,144 Q1260,256 1440,176 L1440,320 L0,320 Z"
  />
</svg>

Three things are worth pointing at. The viewBox is 1440 by 320, which is a ratio and not a measurement. The path ends with L1440,320 L0,320 Z, which runs the outline down the right edge, along the bottom and back up, closing it into a filled band rather than a stroke. And preserveAspectRatio="none" is on the root element, which is what makes the whole thing stretch.

preserveAspectRatio=“none”

By default an SVG keeps its aspect ratio and letterboxes itself inside a wider box. For a divider that is wrong twice over: you want the wave to span the full viewport, and you want the height to stay exactly what you chose.

preserveAspectRatio="none" tells the browser to scale the x and y axes independently until the viewBox fills the element. Give the SVG a CSS width of 100% and a fixed height, and the curve is now an elastic band:

.divider {
  display: block;
  width: 100%;
  height: 160px;
}

@media (min-width: 900px) {
  .divider {
    height: 240px;
  }
}

display: block is not optional. An inline SVG sits on a text baseline and picks up a few pixels of line-height underneath it, which shows up as a thin stripe of the wrong color between the wave and the next section. The other half of that fix is a hairline negative margin, because fractional device pixel ratios can still leave a subpixel seam:

.divider {
  margin-bottom: -1px;
}

The distortion from the independent scaling is a feature. Stretched across a 2560-pixel monitor the wave flattens; squeezed onto a 390-pixel phone it steepens. That is close to what you would draw by hand for each, and nothing in a wave carries detail that has to keep its proportions. If you would rather crop than stretch, drop preserveAspectRatio, wrap the SVG and set overflow: hidden on the wrapper.

Matching the fill to the next section

This is the part people get wrong, and the symptom is always the same: a stripe of an unexpected color at the seam.

The wave is not a third thing between two sections. It belongs to one of them and reaches into the other, so its fill must equal the background of the section it points into.

<section class="hero">
  <h1>Field notes</h1>
  <svg class="divider" viewBox="0 0 1440 320" preserveAspectRatio="none" aria-hidden="true">
    <path fill="#0f172a" d="M0,160 Q180,64 360,128 Q540,192 720,112 Q900,32 1080,144 Q1260,256 1440,176 L1440,320 L0,320 Z" />
  </svg>
</section>

<section class="notes">…</section>
.hero {
  background: #1e8fe5;
}

.notes {
  background: #0f172a;
}

The divider lives at the end of the hero, but it is painted in the color of the section below it. The hero’s blue shows through above the curve, the dark navy fills below, and the two sections meet along the wave. Swap those two colors and the seam breaks.

When the sections are themed, keep the fill in CSS rather than in the markup:

<path fill="currentColor" d="…" />
.divider {
  color: #0f172a;
}

@media (prefers-color-scheme: dark) {
  .divider {
    color: #020617;
  }
}

Inline markup or a CSS background

The same wave also comes out of the generator as a background-image with the SVG encoded as a data URI:

.divider {
  height: 160px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 320' preserveAspectRatio='none'%3E%3Cpath fill='%231e8fe5' d='M0,160 Q180,64 360,128 Q540,192 720,112 Q900,32 1080,144 Q1260,256 1440,176 L1440,320 L0,320 Z'/%3E%3C/svg%3E");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

background-size: 100% 100% is the background equivalent of preserveAspectRatio="none": it stretches the image to the box on both axes. cover would crop instead, which is rarely what a divider wants.

Choose between the two on control, not weight, because both are tiny. Inline markup keeps the fill targetable from CSS, so it can follow currentColor and a theme switch. A background image keeps the markup clean and marks the divider unambiguously as decoration, but the fill is baked into the encoded string, so a second theme needs a second rule with a second copy of the whole wave. One fixed palette: use the background. Anything themed: paste the markup.

Stacking layers

Layers draws the curve two or three times, each copy offset slightly and set at a lower opacity than the one in front:

<svg class="divider" viewBox="0 0 1440 320" preserveAspectRatio="none" aria-hidden="true">
  <path fill="#1e8fe5" fill-opacity="0.35" d="M0,192 Q180,96 360,160 Q540,224 720,144 Q900,64 1080,176 Q1260,288 1440,208 L1440,320 L0,320 Z" />
  <path fill="#1e8fe5" fill-opacity="0.6" d="M0,176 Q180,80 360,144 Q540,208 720,128 Q900,48 1080,160 Q1260,272 1440,192 L1440,320 L0,320 Z" />
  <path fill="#1e8fe5" d="M0,160 Q180,64 360,128 Q540,192 720,112 Q900,32 1080,144 Q1260,256 1440,176 L1440,320 L0,320 Z" />
</svg>

Paint order runs top to bottom, so the faintest copy is written first and the solid one last. The effect reads as depth rather than as a line, and it needs room: at 160 units the three copies overlap too closely to separate and the only result is a muddier color. Keep layers for 320 and 480.

Flipping

Flip decides which side of the curve is solid. Unflipped, the curve runs near the top of the box and the fill hangs below it, so the divider reaches down into the section beneath. Flipped, the curve sits near the bottom and the fill rises above it, which is what closes the section above.

You can flip an existing export in CSS without touching the path:

.divider--flip {
  transform: scaleY(-1);
}

That is fine for a solid fill. For a gradient fill, re-export instead, because flipping the element also flips the gradient and you will find your light stop at the wrong end.

The file is not the problem

A wave is a handful of numbers: one path with three to twelve curve commands, a viewBox and a fill, comfortably under a kilobyte before compression. Three layers triple the path data and it is still smaller than any raster picture of the same edge, and far smaller than the border-radius-and-overflow contortions people reach for instead.

Two habits keep it that way. Do not animate the path itself, because a geometry change forces a repaint every frame; translate the whole element slowly if the divider has to move. And keep the divider out of the accessibility tree, with aria-hidden="true" on inline markup or an empty alt on an image, since it carries no information at all.