GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

CSS Divider: 9 Demos That Compute the Line Shape

A CSS divider looks like a single line, yet its ends cut off hard, the lines do not fit the width of a centered label and a straight section edge looks flat —

See All Nine Divider Scenes Running on This Page

Every cell in the grid plays a short scripted run by itself, and your first real pointer move, key press, scroll or slider drag turns that script off and feeds your input into the same calculation. Nothing is fetched and every length and color is a constant, so what you watch is only how the shape of a line gets worked out.

Auto-plays · click a tile to jump to its section · all nine in one zip

CSS Divider: 9 Demos Where the Line Shape Is Computed

The scenes go from the simplest calculation to the most layered. Scene 01 fades a line out at both ends, and 02 shares the leftover width between two lines around a label. Scene 03 draws a line as you scroll, 04 adds two sine waves into a section edge and 05 cuts a torn edge from seeded noise. Scene 06 sizes dots on a bell curve, 07 decides which items start a row, 08 opens an ornament as the pointer nears and 09 lets a single scroll value move wave, line and color spill together.

Where These Dividers Fit

A landing page that needs a break between sections can take 01, 04 or 09, and a login form with an "or" row between two sign-in options is the job of 02. A css divider with text in the middle is the one case where the two sides must be calculated, because a longer word has to take width from both lines at once. A menu or a tag list wants the css divider vertical between items, which 07 handles by asking JS which item begins a new row. A reading page suits the scroll-drawn line of 03, and an about page with some decoration suits 06 and 08.

How This Set Differs from Neighboring Posts

Memphis Design Patterns: 9 Bold Landing Sections lays dots and waves onto a whole page design, while this set fixes the numbers behind one line or one edge. 9 CSS Screen Transition Animation Effects and 9 Scrollytelling CSS Section Patterns, Copy-Paste change what the screen shows, and neither treats the divider as an element of its own.

Code for Each Divider Technique

The code is plain JS and CSS with no library or CDN. In 03 and 09 the scroll response comes from the scroll event: 03 measures the line with getBoundingClientRect and 09 reads scrollTop. No scene uses IntersectionObserver. Each snippet below is the stretch of source that decides the shape, copied as it stands, while the surrounding markup and styles are in the archive.

01Base Divider Whose Ends Fade to Transparent

The alpha rises from 0 to 1 only inside the outer 30% at each end, and the curve is a smoothstep rather than a straight ramp, so no band shows where the line begins. The same gradient is given to the line and to the soft glow behind it, which means the glow fades in the same shape. It filled 20.4% of the frame at an intensity of 54.5 and moved in 23/23 frames.

Base Divider Whose Ends Fade to Transparent
Open in a new tab

01 · Base Divider Whose Ends Fade to Transparent — Measured area 20.444% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FADE_PCTLINE_H_PX
  function gradientOf(pct) {
    var stops = [];
    var i;
    for (i = 0; i <= STEPS; i += 1) {
      var t = i / STEPS;
      var a = t * t * (3 - 2 * t);
      stops.push('rgba(' + RGB + ',' + a.toFixed(3) + ') ' + (pct * t).toFixed(2) + '%');
    }
    for (i = STEPS; i >= 0; i -= 1) {
      var u = i / STEPS;
      var b = u * u * (3 - 2 * u);
      stops.push('rgba(' + RGB + ',' + b.toFixed(3) + ') ' + (100 - pct * u).toFixed(2) + '%');
    }
    return 'linear-gradient(90deg,' + stops.join(',') + ')';
  }

02Divider That Splits Its Lines Around a Centered Label

The script measures the row and the label, then gives each line (row − label − 2 × LABEL_GAP) / 2 of width, never below 0. A longer word therefore takes equal width from both sides, and the lines never lean to one end. This scene filled 15.2% of the frame at an intensity of 34.0 and moved in 23/23 frames.

Divider That Splits Its Lines Around a Centered Label
Open in a new tab

02 · Divider That Splits Its Lines Around a Centered Label — Measured area 15.208% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LABEL_GAPLINE_H_PX
  function layout() {
    var rowW = row.getBoundingClientRect().width;
    var labelW = label.getBoundingClientRect().width;
    var side = Math.max(0, (rowW - labelW - LABEL_GAP * 2) / 2);
    left.style.width = side.toFixed(1) + 'px';
    right.style.width = side.toFixed(1) + 'px';
    pxEl.textContent = String(Math.round(side));
  }

03Divider Drawn From the Center as the Page Scrolls

How far the line has risen from the bottom of the scroll area, divided by DRAW_RANGE_PX, becomes its length, and a clip-path trims it from both sides at once. Scrolling back shrinks it again because the value is recomputed on every scroll event, not played once. It filled 30.0% of the frame at an intensity of 75.1 and moved in 23/23 frames.

Divider Drawn From the Center as the Page Scrolls
Open in a new tab

03 · Divider Drawn From the Center as the Page Scrolls — Measured area 29.984% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

DRAW_RANGE_PXDRAW_MIN_PCT
  function update() {
    var box = scroller.getBoundingClientRect();
    var lineBox = line.getBoundingClientRect();
    var risen = box.bottom - lineBox.top;
    var p = Math.min(1, Math.max(0, risen / DRAW_RANGE_PX));
    var pct = DRAW_MIN_PCT + (100 - DRAW_MIN_PCT) * p;
    var edge = (100 - pct) / 2;
    line.style.clipPath = 'inset(0 ' + edge.toFixed(2) + '% 0 ' + edge.toFixed(2) + '% round 6px)';
    pctEl.textContent = String(Math.round(pct));
  }

04Wavy Section Edge Built From Two Summed Sines

Two sines with periods of 190 and 83 are added and sampled every 4px into one SVG path, so the edge repeats less than a single curve would. The phase advances each frame, and the button steps the layer count through 1, 2 and 3. This scene filled 11.6% of the frame at an intensity of 113.9 and moved in 23/23 frames.

Wavy Section Edge Built From Two Summed Sines
Open in a new tab

04 · Wavy Section Edge Built From Two Summed Sines — Measured area 11.55% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WAVE_LAYERSWAVE_AMPWAVE_PERIOD
  function edgeY(x) {
    var y = BASE_Y;
    for (var i = 0; i < WAVE_LAYERS; i += 1) {
      y += WAVE_AMP[i] * Math.sin(2 * Math.PI * x / WAVE_PERIOD[i] + phase * WAVE_SPEED[i]);
    }
    return y;
  }
  function draw() {
    var d = 'M0 0H' + VIEW_W + 'V' + edgeY(VIEW_W).toFixed(1);
    for (var x = VIEW_W - STEP_PX; x >= 0; x -= STEP_PX) d += 'L' + x + ' ' + edgeY(x).toFixed(1);
    d += 'L0 ' + edgeY(0).toFixed(1) + 'Z';
    pathEl.setAttribute('d', d);
    layersEl.textContent = String(WAVE_LAYERS);
  }

05Torn-Paper Edge Cut by Seeded Noise

A small generator seeded with a fixed number fills a 256-slot table once, and each vertex height blends a coarse and a fine reading of it before a clip-path polygon cuts the sheet. Because the seed never changes, a reload on the same width gives the same tear. It filled 0.9% of the frame at an intensity of 138.8 and moved in 19/23 frames.

Torn-Paper Edge Cut by Seeded Noise
Open in a new tab

05 · Torn-Paper Edge Cut by Seeded Noise — Measured area 0.949% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

TORN_SEEDTORN_STEPTORN_AMP
  function mulberry32(seed) {
    var a = seed >>> 0;
    return function () {
      a = (a + 0x6D2B79F5) >>> 0;
      var t = a;
      t = Math.imul(t ^ (t >>> 15), t | 1);
      t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }
  var rand = mulberry32(TORN_SEED);
  function noiseAt(k) {
    var big = k / 4;
    var i0 = Math.floor(big);
    var f = smooth(big - i0);
    var coarse = table[i0 % 256] * (1 - f) + table[(i0 + 1) % 256] * f;
    var fine = table[(k * 7 + 3) % 256];
    return 0.65 * coarse + 0.35 * fine;
  }

06Dotted Divider With a Bell-Curve Size and One Glint

Eleven dots take their diameter from a bell curve, so the middle one is 32px and the two outermost are held at the 4px floor. A bright spot then slides across once, and each dot mixes toward white and swells by up to 30% as it passes. The scene filled 4.8% of the frame at an intensity of 94.3 and moved in 19/23 frames.

Dotted Divider With a Bell-Curve Size and One Glint
Open in a new tab

06 · Dotted Divider With a Bell-Curve Size and One Glint — Measured area 4.761% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

DOT_COUNTDOT_MAX_PXGLINT_MS
  for (i = 0; i < DOT_COUNT; i += 1) {
    var d = document.createElement('span');
    d.className = 'dv__dot';
    host.appendChild(d);
    dots.push(d);
    var gauss = Math.exp(-Math.pow(i - center, 2) / (2 * sigma * sigma));
    sizes.push(Math.max(DOT_MIN_PX, DOT_MAX_PX * gauss));
  }
  function paint(g) {
    for (var k = 0; k < DOT_COUNT; k += 1) {
      var b = g === null ? 0 : Math.exp(-Math.pow(k - g, 2) / (2 * GLINT_WIDTH * GLINT_WIDTH));
      var px = sizes[k] * (1 + 0.3 * b);
      var c = [0, 1, 2].map(function (n) { return Math.round(BASE[n] + (LIGHT[n] - BASE[n]) * b); });
      var el = dots[k];
      el.style.width = px.toFixed(1) + 'px';
      el.style.height = px.toFixed(1) + 'px';
      el.style.background = 'rgb(' + c.join(',') + ')';
    }
  }

07Vertical Separator That Never Leads a Wrapped Row

The line is a ::before pseudo-element on each item, and mark() compares every item's offsetTop with the one before it and adds is-first to the first item of each row. That class removes the item's left padding and sets the line's opacity to 0. It filled 29.5% of the frame at an intensity of 104.8 and moved in 23/23 frames.

Vertical Separator That Never Leads a Wrapped Row
Open in a new tab

07 · Vertical Separator That Never Leads a Wrapped Row — Measured area 29.527% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SEP_GAPSEP_H_PX
  function mark() {
    var rows = 0;
    var lastTop = null;
    items.forEach(function (li) {
      var top = li.offsetTop;
      var first = lastTop === null || top !== lastTop;
      if (first) rows += 1;
      li.classList.toggle('is-first', first);
      lastTop = top;
    });
    rowsEl.textContent = String(rows);
  }

08Divider Whose Ornament Opens as the Pointer Nears

The target opening is a smoothstep of how close the pointer is, and the shown value follows it by a share of the remaining gap each frame, so the opening trails the pointer. The two lines slide apart while the ornament turns and grows. This scene filled 2.3% of the frame at an intensity of 159.9 and moved in 23/23 frames.

Divider Whose Ornament Opens as the Pointer Nears
Open in a new tab

08 · Divider Whose Ornament Opens as the Pointer Nears — Measured area 2.326% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

NEAR_RANGEOPEN_RATEOPEN_MAX_PX
  function smooth(t) { return t * t * (3 - 2 * t); }
  function paint() {
    var half = OPEN_MAX_PX * open / 2;
    left.style.transform = 'translateX(' + (-half).toFixed(2) + 'px)';
    right.style.transform = 'translateX(' + half.toFixed(2) + 'px)';
    orn.style.transform = 'rotate(' + (open * 135).toFixed(1) + 'deg) scale(' + (0.5 + open * 0.9).toFixed(3) + ')';
    openEl.textContent = String(Math.round(open * 100));
  }
  function goalOf() {
    if (pinned) return 1;
    if (!pointer) return 0;
    var box = orn.getBoundingClientRect();
    var dx = pointer.x - (box.left + box.width / 2);
    var dy = pointer.y - (box.top + box.height / 2);
    var dist = Math.sqrt(dx * dx + dy * dy);
    return smooth(Math.min(1, Math.max(0, 1 - dist / NEAR_RANGE)));
  }

09Scene Where Two Sections Hand Off Across a Wave Divider

One scrollTop value sets the wave phase, how much of the edge line is drawn and how high the upper color spills into the lower section. The edge is drawn by stroke-dashoffset on a path of length 1, so it needs no measuring. It filled 26.0% of the frame at an intensity of 82.1 and moved in 23/23 frames.

Scene Where Two Sections Hand Off Across a Wave Divider
Open in a new tab

09 · Scene Where Two Sections Hand Off Across a Wave Divider — Measured area 25.993% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WAVE_PHASE_PER_PXBLEED_PCTDRAW_RANGE_PX
  function edgeY(x, phase) {
    var y = BASE_Y;
    for (var i = 0; i < WAVE_AMP.length; i += 1) {
      y += WAVE_AMP[i] * Math.sin(2 * Math.PI * x / WAVE_PERIOD[i] + phase * WAVE_SPEED[i]);
    }
    return y;
  }
  function update() {
    var top = scroller.scrollTop;
    var phase = top * WAVE_PHASE_PER_PX;
    var pts = pathOf(phase);
    fill.setAttribute('d', 'M0 0H' + VIEW_W + 'V' + pts[pts.length - 1].split(' ')[1] + 'L' + pts.slice().reverse().join('L') + 'Z');
    var line = 'M' + pts.join('L');
    edge.setAttribute('d', line);
    var drawn = Math.min(1, Math.max(0, top / DRAW_RANGE_PX));
    edge.style.strokeDasharray = '1';
    edge.style.strokeDashoffset = String(1 - drawn);
    bleed.style.height = (BLEED_PCT * drawn).toFixed(1) + '%';
    phaseEl.textContent = phase.toFixed(1);
  }

Where the Values Change the Scene — Fade Length, Wave Layers, Reaction Range

Three constants decide most of how far a line stays solid, how tall the edge swings and how near the pointer must be. The table comes from rendering each scene and measuring the 23 gaps between its 24 captured frames.

Fade Length Sets the Solid Width of the Line

At FADE_PCT 30 the smoothstep passes half alpha at 15% from the end. Dropping it to 10 keeps the line solid almost to the tips, and 50 makes the two fades meet so only the center is fully solid.

Wave Layers Set the Swing of the Edge

With 2 layers the summed amplitude is 25 and the edge moves between y 37 and 87, while 3 layers make it 30 and a range of 32 to 92. The third layer has a period of 47, so the 4px sampling gives about twelve samples per cycle.

Reaction Range Sets How Far the Ornament Opens

At NEAR_RANGE 120 the target opening is 0.5 when the pointer is 60px away and about 0.16 at 90px, and it stays shut from 120px out. Widening the range to 200 opens it much further at the same 90px.

Item Cumulative area % Mean intensity Moving frames
01 Fading ends 20.4% 54.5 23/23
02 Label split 15.2% 34.0 23/23
03 Scroll draw 30.0% 75.1 23/23
04 Sine edge 11.6% 113.9 23/23
05 Torn paper 0.9% 138.8 19/23
06 Bell-curve dots 4.8% 94.3 19/23
07 Vertical separator 29.5% 104.8 23/23
08 Ornament opens 2.3% 159.9 23/23
09 Wave handoff 26.0% 82.1 23/23

Scenes 05, 06 and 08 fall under 10% because a thin line or a few dots cover little of the frame. Scenes 05 and 06 read 19/23 because their scripted run ends at 1600 ms and rests until the cycle restarts.

Where It Breaks — Wrapped Rows, Narrow Screens, Unreadable Labels

Fade Length Past 50 Twists the Gradient

The left fade ends where the right fade starts at 50%. Beyond that the two fades would overlap and their stops would cross, so the slider in 01 stops at 50.

A Long Label Squeezes Both Lines to Nothing

When the label plus 24px of gap is wider than the row, both lines reach 0px and the label spills out of the row, because it is set to white-space: nowrap. The fix on a narrow screen is a shorter label.

Waves Shorter Than the Sample Step Become Stairs

Scene 04 samples every 4px, so a wave with a period of 8 or less is missed between samples and shows steps. When you add a layer, keep its period above 8.

The Same Seed Tears Differently on Another Width

Scene 05 counts its points from the sheet width divided by 9, so another width stretches or squeezes the same noise into a different shape. Only on the same width does a reload give the same tear.

A Resize Without a Fresh Check Leaves the Old Line

Scene 07 calls mark() only from setWidth(), and it has no resize listener. If the window changes size by another route, the old is-first marks stay, and items of different heights break the offsetTop comparison even on one row.

To open the full HTML and SCSS and try other values, unzip the archive below with the password ntadr22m and each of the nine folders holds one demo page with its stylesheet.

Three Variants and Accessibility

Item Constant Value What changes
01 FADE_PCT 30 → 10 The line stays solid nearer its ends, and 50 leaves only the center point fully solid.
04 WAVE_LAYERS 2 → 3 A ripple with a period of 47 joins, the edge repeats less and the summed amplitude grows from 25 to 30.
08 NEAR_RANGE 120 → 200 The ornament starts opening 200px from its center, so the same pointer spot opens it further.

Five scenes have a native button, namely 02, 04, 05, 06 and 08, while 01 and 07 use a slider and 03 and 09 are scroll areas with a tabindex, so arrow keys and PageDown reach them. When prefers-reduced-motion is reduce, no scene starts its animation loop and each draws its final state at once. Scene 01 stays at 30, 04 stops at phase 0, 05 draws the finished tear, 03 sits at the middle of its scroll range, and 09 at 31% of it, half of SCROLL_SHARE 0.62, and each draws once. Any input after that is applied immediately, with no easing.

Is One border Line Enough for a CSS Divider?

When the line is a straight run in one color and one thickness, a single border is enough. It cannot fade its ends, fit itself around a label or turn a section edge into a curve, so add the calculation only where that gap shows, as 01, 02 and 04 do.

For other kinds of motion, 9 CSS Scroll Progress Bar Ideas, Copy-Paste follows the same scroll value into a progress bar, and the screen-change post above covers what happens when a whole view swaps.

Frequently Asked Questions Before Building a CSS Divider

Can a CSS Divider Between Flex Items Use the Same Method as the Label Row?

Only partly. For a label between two lines a flex row with two growing lines works, but 02 measures and sets the widths so a long label cannot hide a line unnoticed. For a separator between menu items, 07 is the closer fit.

What Turns a Divider Into a Section Edge?

The edge of a colored block has to be a curve rather than a line, so 04 builds an SVG path and 05 cuts a clip-path. Scene 09 adds the scroll value so the upper color spills downward as you move.

Can I Make a Divider Respond to Scroll or the Pointer?

Yes. Scenes 03 and 09 follow the scroll event and 08 follows pointer distance, and each scene keeps its first shape until input arrives.

Enter the archive password

The password is inside this article. You will find it as you read.