Pure TailwindMIT licensed

Tailwind CSS SVG Wave Section Divider with Zero Dependencies

The baseline everyone actually needs, three ways: an inline SVG path, the same wave as a reusable mask-image utility, and a low-amplitude editorial seam. The port's win is text-twd01-next [&_path]:fill-current — the seam colour now lives on the SVG that renders it, so re-theming one divider needs no new CSS rule.

Live Demo
Plain CSSTailwind v4
Try it

Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49

Cruip · Affiliate link

Browse Cruip templates

The code

<section class="twd-01 w-full min-h-svh flex flex-col font-twd01-sans">

  <!-- Classic inline SVG wave -->
  <div class="relative bg-twd01-ink text-white px-6 pt-18 pb-27 text-center">
    <p class="font-twd01-mono text-xs tracking-[0.18em] uppercase text-twd01-mute mb-3.5">Pure Tailwind + inline SVG</p>
    <h2 class="text-[clamp(26px,4vw,40px)] leading-[1.1] font-semibold mb-2.5">Break the boxy grid</h2>
    <p class="text-[15px] text-twd01-onink">One path, one class list, zero dependencies.</p>
    <svg class="absolute inset-x-0 -bottom-px block w-full h-[clamp(56px,9vw,110px)] text-twd01-next [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">
      <path d="M0,64 C240,120 480,8 720,40 C960,72 1200,120 1440,72 L1440,120 L0,120 Z"></path>
    </svg>
  </div>
  <div class="bg-twd01-next text-twd01-ink px-6 pt-11 pb-16 text-center">
    <p class="text-[15px] text-twd01-body">Next section starts here &mdash; the wave is this block&rsquo;s own edge.</p>
  </div>

  <!-- Reusable data-URI mask: one arbitrary property, any two sections -->
  <div class="bg-twd01-teal text-white px-6 pt-16 pb-10 text-center">
    <h2 class="text-[clamp(24px,3.6vw,34px)] font-semibold mb-2">Reusable divider utility</h2>
    <p class="text-[15px] text-twd01-onteal">The wave lives in a custom property, so the markup stays a single div.</p>
  </div>
  <div class="bg-twd01-teal">
    <div role="presentation" class="-mt-px h-[clamp(48px,7vw,88px)] bg-twd01-pale [mask:var(--twd01-wave)_no-repeat_center/100%_100%]"></div>
  </div>
  <div class="bg-twd01-pale text-twd01-teal px-6 pt-7 pb-15 text-center">
    <p class="text-[15px]">Drop the masked div between any two blocks &mdash; <code class="font-twd01-mono text-[13.5px] bg-twd01-tint px-1.5 py-0.5 rounded">[mask:var(--twd01-wave)&hellip;]</code></p>
  </div>

  <!-- Soft double-curve seam -->
  <div class="relative bg-twd01-warm text-twd01-deep px-6 pt-17 pb-24 text-center">
    <h2 class="text-[clamp(24px,3.6vw,34px)] font-semibold">Gentle, editorial seam</h2>
    <svg class="absolute inset-x-0 -bottom-px block w-full h-[clamp(44px,6vw,80px)] text-twd01-deep [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 90" preserveAspectRatio="none" aria-hidden="true" focusable="false">
      <path d="M0,52 C180,4 360,4 540,44 C740,88 900,88 1080,50 C1220,20 1330,20 1440,42 L1440,90 L0,90 Z"></path>
    </svg>
  </div>
  <div class="bg-twd01-deep text-twd01-warm px-6 pt-8 pb-16 text-center">
    <p class="text-[15px]">Low-amplitude curves read as premium; tall waves read as playful.</p>
  </div>

</section>
@import "tailwindcss";

@theme {
  --color-twd01-ink: #2d3555;
  --color-twd01-ink: oklch(0.28 0.06 264);
  --color-twd01-onink: #d9dcea;
  --color-twd01-onink: oklch(0.88 0.02 264);
  --color-twd01-mute: #a8aec8;
  --color-twd01-mute: oklch(0.74 0.03 264);
  --color-twd01-next: #f5f6fa;
  --color-twd01-next: oklch(0.97 0.01 264);
  --color-twd01-body: #4a5170;
  --color-twd01-body: oklch(0.42 0.04 264);
  --color-twd01-teal: #1c7f9e;
  --color-twd01-teal: oklch(0.55 0.16 205);
  --color-twd01-onteal: #dff1f7;
  --color-twd01-onteal: oklch(0.93 0.02 205);
  --color-twd01-pale: #f7fbfc;
  --color-twd01-pale: oklch(0.98 0.01 205);
  --color-twd01-tint: #d7eaf1;
  --color-twd01-tint: oklch(0.92 0.03 205);
  --color-twd01-warm: #f4e6c8;
  --color-twd01-warm: oklch(0.93 0.05 78);
  --color-twd01-deep: #6b3a2c;
  --color-twd01-deep: oklch(0.35 0.07 30);
  --font-twd01-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-twd01-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.twd-01 {
  width: 100%;
  min-height: 100svh;
}
/* The wave shape for the mask variant: geometry, not a design-scale value, so it stays
   a plain custom property instead of an @theme entry. */

.twd-01 {
  --twd01-wave: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20100'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,40%20C300,110%20640,-10%20960,34%20C1200,68%201320,74%201440,44%20L1440,100%20L0,100%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim — no variant form exists for the -webkit- prefix. */

.twd-01 [role="presentation"] {
  -webkit-mask: var(--twd01-wave) no-repeat center/100% 100%;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-twd01-ink: #2d3555;
  --color-twd01-ink: oklch(0.28 0.06 264);
  --color-twd01-onink: #d9dcea;
  --color-twd01-onink: oklch(0.88 0.02 264);
  --color-twd01-mute: #a8aec8;
  --color-twd01-mute: oklch(0.74 0.03 264);
  --color-twd01-next: #f5f6fa;
  --color-twd01-next: oklch(0.97 0.01 264);
  --color-twd01-body: #4a5170;
  --color-twd01-body: oklch(0.42 0.04 264);
  --color-twd01-teal: #1c7f9e;
  --color-twd01-teal: oklch(0.55 0.16 205);
  --color-twd01-onteal: #dff1f7;
  --color-twd01-onteal: oklch(0.93 0.02 205);
  --color-twd01-pale: #f7fbfc;
  --color-twd01-pale: oklch(0.98 0.01 205);
  --color-twd01-tint: #d7eaf1;
  --color-twd01-tint: oklch(0.92 0.03 205);
  --color-twd01-warm: #f4e6c8;
  --color-twd01-warm: oklch(0.93 0.05 78);
  --color-twd01-deep: #6b3a2c;
  --color-twd01-deep: oklch(0.35 0.07 30);
  --font-twd01-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-twd01-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.twd-01 {
  width: 100%;
  min-height: 100svh;
}
/* The wave shape for the mask variant: geometry, not a design-scale value, so it stays
   a plain custom property instead of an @theme entry. */

.twd-01 {
  --twd01-wave: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20100'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,40%20C300,110%20640,-10%20960,34%20C1200,68%201320,74%201440,44%20L1440,100%20L0,100%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim — no variant form exists for the -webkit- prefix. */

.twd-01 [role="presentation"] {
  -webkit-mask: var(--twd01-wave) no-repeat center/100% 100%;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • .wave path{fill:currentColor} becomes [&_path]:fill-current on the SVG itself. The arbitrary descendant variant looks heavier than the CSS rule for one divider and is dramatically lighter for fourteen — there is no stylesheet to open.
  • The clamp() height is the clearest win in the collection: the CSS original needed a declaration in a stylesheet, the port needs h-[clamp(56px,9vw,110px)] next to the geometry it governs.
  • The mask data URI stays in CSS with its spaces percent-encoded, so the arbitrary property stays legible; the underscore-escaping alternative is unreadable at this length.
  • UI change from the CSS original: the masked divider now sits inside a parent carrying the previous section's colour. A mask reveals whatever is behind it, and the original assumed a white page — on any tinted background the area above the curve showed white instead of the section it flows from.
  • UI change from the CSS original: body copy was opacity:.78 over a dark surface, which drops effective contrast below 4.5:1 at 15px. It is now a full-opacity token (text-twd01-onink, --color-twd01-body) at the same apparent lightness.
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 demo 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: Tailwind CSS SVG Wave Section Divider with Zero Dependencies
Source: https://codefronts.com/layouts/tailwind-css-wave-section-dividers/tailwind-css-svg-wave-section-divider-zero-dependencies/

The baseline everyone actually needs, three ways: an inline SVG path, the same wave as a reusable mask-image utility, and a low-amplitude editorial seam. The port's win is text-twd01-next [&_path]:fill-current — the seam colour now lives on the SVG that renders it, so re-theming one divider needs no new CSS rule.
## HTML
```html
<section class="twd-01 w-full min-h-svh flex flex-col font-twd01-sans">

  <!-- Classic inline SVG wave -->
  <div class="relative bg-twd01-ink text-white px-6 pt-18 pb-27 text-center">
    <p class="font-twd01-mono text-xs tracking-[0.18em] uppercase text-twd01-mute mb-3.5">Pure Tailwind + inline SVG</p>
    <h2 class="text-[clamp(26px,4vw,40px)] leading-[1.1] font-semibold mb-2.5">Break the boxy grid</h2>
    <p class="text-[15px] text-twd01-onink">One path, one class list, zero dependencies.</p>
    <svg class="absolute inset-x-0 -bottom-px block w-full h-[clamp(56px,9vw,110px)] text-twd01-next [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">
      <path d="M0,64 C240,120 480,8 720,40 C960,72 1200,120 1440,72 L1440,120 L0,120 Z"></path>
    </svg>
  </div>
  <div class="bg-twd01-next text-twd01-ink px-6 pt-11 pb-16 text-center">
    <p class="text-[15px] text-twd01-body">Next section starts here &mdash; the wave is this block&rsquo;s own edge.</p>
  </div>

  <!-- Reusable data-URI mask: one arbitrary property, any two sections -->
  <div class="bg-twd01-teal text-white px-6 pt-16 pb-10 text-center">
    <h2 class="text-[clamp(24px,3.6vw,34px)] font-semibold mb-2">Reusable divider utility</h2>
    <p class="text-[15px] text-twd01-onteal">The wave lives in a custom property, so the markup stays a single div.</p>
  </div>
  <div class="bg-twd01-teal">
    <div role="presentation" class="-mt-px h-[clamp(48px,7vw,88px)] bg-twd01-pale [mask:var(--twd01-wave)_no-repeat_center/100%_100%]"></div>
  </div>
  <div class="bg-twd01-pale text-twd01-teal px-6 pt-7 pb-15 text-center">
    <p class="text-[15px]">Drop the masked div between any two blocks &mdash; <code class="font-twd01-mono text-[13.5px] bg-twd01-tint px-1.5 py-0.5 rounded">[mask:var(--twd01-wave)&hellip;]</code></p>
  </div>

  <!-- Soft double-curve seam -->
  <div class="relative bg-twd01-warm text-twd01-deep px-6 pt-17 pb-24 text-center">
    <h2 class="text-[clamp(24px,3.6vw,34px)] font-semibold">Gentle, editorial seam</h2>
    <svg class="absolute inset-x-0 -bottom-px block w-full h-[clamp(44px,6vw,80px)] text-twd01-deep [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 90" preserveAspectRatio="none" aria-hidden="true" focusable="false">
      <path d="M0,52 C180,4 360,4 540,44 C740,88 900,88 1080,50 C1220,20 1330,20 1440,42 L1440,90 L0,90 Z"></path>
    </svg>
  </div>
  <div class="bg-twd01-deep text-twd01-warm px-6 pt-8 pb-16 text-center">
    <p class="text-[15px]">Low-amplitude curves read as premium; tall waves read as playful.</p>
  </div>

</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-twd01-ink: #2d3555;
  --color-twd01-ink: oklch(0.28 0.06 264);
  --color-twd01-onink: #d9dcea;
  --color-twd01-onink: oklch(0.88 0.02 264);
  --color-twd01-mute: #a8aec8;
  --color-twd01-mute: oklch(0.74 0.03 264);
  --color-twd01-next: #f5f6fa;
  --color-twd01-next: oklch(0.97 0.01 264);
  --color-twd01-body: #4a5170;
  --color-twd01-body: oklch(0.42 0.04 264);
  --color-twd01-teal: #1c7f9e;
  --color-twd01-teal: oklch(0.55 0.16 205);
  --color-twd01-onteal: #dff1f7;
  --color-twd01-onteal: oklch(0.93 0.02 205);
  --color-twd01-pale: #f7fbfc;
  --color-twd01-pale: oklch(0.98 0.01 205);
  --color-twd01-tint: #d7eaf1;
  --color-twd01-tint: oklch(0.92 0.03 205);
  --color-twd01-warm: #f4e6c8;
  --color-twd01-warm: oklch(0.93 0.05 78);
  --color-twd01-deep: #6b3a2c;
  --color-twd01-deep: oklch(0.35 0.07 30);
  --font-twd01-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-twd01-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.twd-01 {
  width: 100%;
  min-height: 100svh;
}
/* The wave shape for the mask variant: geometry, not a design-scale value, so it stays
   a plain custom property instead of an @theme entry. */

.twd-01 {
  --twd01-wave: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20100'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,40%20C300,110%20640,-10%20960,34%20C1200,68%201320,74%201440,44%20L1440,100%20L0,100%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim — no variant form exists for the -webkit- prefix. */

.twd-01 [role="presentation"] {
  -webkit-mask: var(--twd01-wave) no-repeat center/100% 100%;
}
```

How this works

The wave is the next section's own edge. The SVG is absolutely positioned on the seam and filled with the colour of the section it flows into; preserveAspectRatio="none" plus w-full stretches it fluidly instead of cropping.

<svg class="absolute inset-x-0 -bottom-px block w-full h-[clamp(56px,9vw,110px)]
            text-twd01-next [&_path]:fill-current pointer-events-none"
     viewBox="0 0 1440 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">

The mask variant is the reusable one. The path moves into a data-URI custom property in the stylesheet and the divider becomes a <div> wearing one arbitrary property — no SVG node in the markup at all, and the shape is swappable per breakpoint or per theme by reassigning one variable.

Amplitude is art direction. Variant c keeps the same machinery at half the height: low-amplitude curves read premium, tall ones read playful. Nothing about the code changes — only the clamp() and the control points.

Make it yours

  • Height: edit the three numbers in h-[clamp(...)]; 48–160px covers most layouts.
  • Flip the wave with -scale-y-100, mirror it with -scale-x-100 — both are single utilities in v4.
  • Retheme through --color-twd01-next: the SVG reads it via text-* and the path via fill-current.
  • Swap the masked shape by reassigning --twd01-wave inside a media query or a data-theme block.

Gotchas — read before shipping

  • Keep block on the SVG. Without it the inline baseline leaves a 4–5px strip of the wrong colour under the curve — the single most reported wave bug.
  • preserveAspectRatio="none" is not optional: the default preserves the ratio and crops the crest on wide screens.
  • Close the path with L…Z down to the viewBox floor or the fill leaks and a sliver of the previous section shows through.
  • pointer-events-none on every wave. A 110px curve sitting over a CTA will eat the click otherwise.
  • Tailwind's mask-* utilities cover the image and position separately; a single-shorthand [mask:...] arbitrary property is clearer for a full mask composition — but ship the -webkit-mask shim in CSS for Safari.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Inline SVG with a viewBox is universally supported; the quoted floor is Tailwind v4's own generated sheet. The mask variant needs the -webkit-mask shim below Safari 15.4, where it otherwise renders as a straight-edged band.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Arbitraryneeds bracket syntax

Search CodeFronts

Loading…