13 / 14
All 14 in 14 Tailwind CSS Wave Section Dividers
- 01Tailwind CSS SVG Wave Section Divider with Zero Dependencies
- 02Tailwind Animated Wave Section Divider for Landing Pages
- 03Tailwind Responsive SVG Wave Divider at the Bottom of a Hero
- 04Tailwind Stacked Multi Color SVG Wave Section Divider
- 05Tailwind Wave Section Divider Footer Transition
- 06Pure Tailwind Clip Path Wave Divider Without SVG
- 07Tailwind Inverted Curve SVG Wave Divider
- 08Tailwind Wave Divider Full Width Responsive Mobile Fix
- 09Tailwind Gradient Wave Section Divider with a Background Mask
- 10Tailwind Asymmetrical Organic SVG Section Wave
- 11Tailwind Dark Mode Wave Divider Colour Match
- 12Tailwind Animated Sine Wave Section Divider Transition
- 13Tailwind Wave Section Divider with an Overlay Background Image
- 14Tailwind Vertical Wave Divider for Side Navigation
Tailwind Wave Section Divider with an Overlay Background Image
A crisp wave over a full-bleed photo, with the contrast scrim, focal-point crop and LCP hygiene handled properly — the combination that usually goes wrong when a divider is dropped onto imagery. The second variant masks the photo itself into a wave-edged ribbon.
Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49
Cruip · Affiliate link
The code
<section class="twd-13 w-full min-h-svh flex flex-col bg-twd13-paper font-twd13-sans">
<!-- Photo hero with scrim + wave -->
<header class="relative isolate overflow-hidden text-white px-6 pt-21 pb-[clamp(96px,13vw,150px)] text-center before:content-[''] before:absolute before:inset-0 before:-z-10 before:bg-[linear-gradient(180deg,oklch(0.15_0.03_260/0.3),oklch(0.13_0.03_260/0.82))]">
<img src="https://picsum.photos/seed/twd13a/1600/900" width="1600" height="900" alt="" fetchpriority="high" decoding="async" class="absolute inset-0 -z-20 w-full h-full object-cover object-[50%_35%]">
<div class="relative max-w-[46ch] mx-auto">
<p class="text-xs tracking-[0.2em] uppercase text-twd13-eyebrow mb-3">Full-bleed imagery</p>
<h2 class="text-[clamp(27px,4.6vw,46px)] leading-[1.06] font-semibold mb-2.5 [text-shadow:0_2px_18px_oklch(0.15_0.03_260/0.55)]">Legible over any photo</h2>
<p class="text-[15px]">Gradient scrim below, wave on top, text in the safe zone.</p>
</div>
<svg class="absolute inset-x-0 -bottom-px z-10 block w-full h-[clamp(56px,9vw,116px)] text-twd13-next [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">
<path d="M0,62 C280,118 560,10 840,54 C1080,92 1250,106 1440,74 L1440,120 L0,120 Z"></path>
</svg>
</header>
<div class="bg-twd13-next text-twd13-nextink px-6 pt-6 pb-15 text-center">
<p class="text-[15px]">The wave fill matches this section exactly — no sticker effect.</p>
</div>
<!-- Wave-masked image band -->
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px]">Text section</p>
</div>
<div role="img" aria-label="Coastline photograph shown inside a wave-shaped band" class="-my-6 h-[clamp(220px,32vw,380px)] bg-twd13-bandfallback bg-[url('https://picsum.photos/seed/twd13b/1600/700')] bg-cover bg-center bg-no-repeat [mask:var(--twd13-band)_no-repeat_center/100%_100%]"></div>
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px] max-w-[52ch] mx-auto">The photo itself is masked into a wave band — both edges curved, no SVG node.</p>
</div>
</section><section class="twd-13 w-full min-h-svh flex flex-col bg-twd13-paper font-twd13-sans">
<!-- Photo hero with scrim + wave -->
<header class="relative isolate overflow-hidden text-white px-6 pt-21 pb-[clamp(96px,13vw,150px)] text-center before:content-[''] before:absolute before:inset-0 before:-z-10 before:bg-[linear-gradient(180deg,oklch(0.15_0.03_260/0.3),oklch(0.13_0.03_260/0.82))]">
<img src="https://picsum.photos/seed/twd13a/1600/900" width="1600" height="900" alt="" fetchpriority="high" decoding="async" class="absolute inset-0 -z-20 w-full h-full object-cover object-[50%_35%]">
<div class="relative max-w-[46ch] mx-auto">
<p class="text-xs tracking-[0.2em] uppercase text-twd13-eyebrow mb-3">Full-bleed imagery</p>
<h2 class="text-[clamp(27px,4.6vw,46px)] leading-[1.06] font-semibold mb-2.5 [text-shadow:0_2px_18px_oklch(0.15_0.03_260/0.55)]">Legible over any photo</h2>
<p class="text-[15px]">Gradient scrim below, wave on top, text in the safe zone.</p>
</div>
<svg class="absolute inset-x-0 -bottom-px z-10 block w-full h-[clamp(56px,9vw,116px)] text-twd13-next [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">
<path d="M0,62 C280,118 560,10 840,54 C1080,92 1250,106 1440,74 L1440,120 L0,120 Z"></path>
</svg>
</header>
<div class="bg-twd13-next text-twd13-nextink px-6 pt-6 pb-15 text-center">
<p class="text-[15px]">The wave fill matches this section exactly — no sticker effect.</p>
</div>
<!-- Wave-masked image band -->
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px]">Text section</p>
</div>
<div role="img" aria-label="Coastline photograph shown inside a wave-shaped band" class="-my-6 h-[clamp(220px,32vw,380px)] bg-twd13-bandfallback bg-[url('https://picsum.photos/seed/twd13b/1600/700')] bg-cover bg-center bg-no-repeat [mask:var(--twd13-band)_no-repeat_center/100%_100%]"></div>
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px] max-w-[52ch] mx-auto">The photo itself is masked into a wave band — both edges curved, no SVG node.</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-twd13-next: #f6f7fa;
--color-twd13-next: oklch(0.97 0.01 250);
--color-twd13-nextink: #3f4655;
--color-twd13-nextink: oklch(0.42 0.03 250);
--color-twd13-eyebrow: #dfe4ee;
--color-twd13-eyebrow: oklch(0.91 0.01 250);
--color-twd13-paper: #f3f5f8;
--color-twd13-paper: oklch(0.96 0.01 230);
--color-twd13-paperink: #414a55;
--color-twd13-paperink: oklch(0.43 0.03 230);
--color-twd13-bandfallback: #35657f;
--color-twd13-bandfallback: oklch(0.45 0.07 230);
--font-twd13-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The ribbon cutter: a wave on the top AND bottom edge of one mask. */
.twd-13 {
width: 100%;
min-height: 100svh;
--twd13-band: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20400'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,70%20C360,-10%20700,120%201000,70%20C1180,40%201300,40%201440,66%20L1440,330%20C1200,410%20900,270%20620,326%20C420,366%20180,366%200,334%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the photo band. */
.twd-13 [role="img"] {
-webkit-mask: var(--twd13-band) no-repeat center/100% 100%;
}@import "tailwindcss";
@theme {
--color-twd13-next: #f6f7fa;
--color-twd13-next: oklch(0.97 0.01 250);
--color-twd13-nextink: #3f4655;
--color-twd13-nextink: oklch(0.42 0.03 250);
--color-twd13-eyebrow: #dfe4ee;
--color-twd13-eyebrow: oklch(0.91 0.01 250);
--color-twd13-paper: #f3f5f8;
--color-twd13-paper: oklch(0.96 0.01 230);
--color-twd13-paperink: #414a55;
--color-twd13-paperink: oklch(0.43 0.03 230);
--color-twd13-bandfallback: #35657f;
--color-twd13-bandfallback: oklch(0.45 0.07 230);
--font-twd13-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The ribbon cutter: a wave on the top AND bottom edge of one mask. */
.twd-13 {
width: 100%;
min-height: 100svh;
--twd13-band: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20400'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,70%20C360,-10%20700,120%201000,70%20C1180,40%201300,40%201440,66%20L1440,330%20C1200,410%20900,270%20620,326%20C420,366%20180,366%200,334%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the photo band. */
.twd-13 [role="img"] {
-webkit-mask: var(--twd13-band) no-repeat center/100% 100%;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-twd13-next: #f6f7fa;
--color-twd13-next: oklch(0.97 0.01 250);
--color-twd13-nextink: #3f4655;
--color-twd13-nextink: oklch(0.42 0.03 250);
--color-twd13-eyebrow: #dfe4ee;
--color-twd13-eyebrow: oklch(0.91 0.01 250);
--color-twd13-paper: #f3f5f8;
--color-twd13-paper: oklch(0.96 0.01 230);
--color-twd13-paperink: #414a55;
--color-twd13-paperink: oklch(0.43 0.03 230);
--color-twd13-bandfallback: #35657f;
--color-twd13-bandfallback: oklch(0.45 0.07 230);
--font-twd13-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The ribbon cutter: a wave on the top AND bottom edge of one mask. */
.twd-13 {
width: 100%;
min-height: 100svh;
--twd13-band: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20400'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,70%20C360,-10%20700,120%201000,70%20C1180,40%201300,40%201440,66%20L1440,330%20C1200,410%20900,270%20620,326%20C420,366%20180,366%200,334%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the photo band. */
.twd-13 [role="img"] {
-webkit-mask: var(--twd13-band) no-repeat center/100% 100%;
}@import "tailwindcss";
@theme {
--color-twd13-next: #f6f7fa;
--color-twd13-next: oklch(0.97 0.01 250);
--color-twd13-nextink: #3f4655;
--color-twd13-nextink: oklch(0.42 0.03 250);
--color-twd13-eyebrow: #dfe4ee;
--color-twd13-eyebrow: oklch(0.91 0.01 250);
--color-twd13-paper: #f3f5f8;
--color-twd13-paper: oklch(0.96 0.01 230);
--color-twd13-paperink: #414a55;
--color-twd13-paperink: oklch(0.43 0.03 230);
--color-twd13-bandfallback: #35657f;
--color-twd13-bandfallback: oklch(0.45 0.07 230);
--font-twd13-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The ribbon cutter: a wave on the top AND bottom edge of one mask. */
.twd-13 {
width: 100%;
min-height: 100svh;
--twd13-band: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20400'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,70%20C360,-10%20700,120%201000,70%20C1180,40%201300,40%201440,66%20L1440,330%20C1200,410%20900,270%20620,326%20C420,366%20180,366%200,334%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the photo band. */
.twd-13 [role="img"] {
-webkit-mask: var(--twd13-band) no-repeat center/100% 100%;
}<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&display=swap">
<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&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The scrim as a
before:pseudo-element with-z-10is a faithful port, and havingisolatein the same class list as the two negative z-indexes it contains is a real readability gain — that relationship is invisible in a stylesheet. object-[50%_35%]for the focal point is the kind of value that belongs inline: it is a decision about one photograph, not a design-system token.- The band's mask stays a custom property for the same reason as 01 and 09 — a 240-character data URI inside a class attribute is not something anyone should have to read twice.
- UI change from the CSS original: the masked block and the photo band now sit on a root that carries the page colour, so the transparent tail below each curve reveals the next section's paper rather than a white band — the original only worked on a white page.
- UI change from the CSS original: the hero eyebrow was
opacity:.78white and the body copy.88, both of which drift under the lighter top of the scrim. Both are now full-opacity (a token for the eyebrow, plain white for the copy), and the scrim's bottom stop went from 0.8 to 0.82 to hold the headline at AAA over a bright photo.
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 Wave Section Divider with an Overlay Background Image
Source: https://codefronts.com/layouts/tailwind-css-wave-section-dividers/tailwind-wave-section-divider-overlay-background-image/
A crisp wave over a full-bleed photo, with the contrast scrim, focal-point crop and LCP hygiene handled properly — the combination that usually goes wrong when a divider is dropped onto imagery. The second variant masks the photo itself into a wave-edged ribbon.
## HTML
```html
<section class="twd-13 w-full min-h-svh flex flex-col bg-twd13-paper font-twd13-sans">
<!-- Photo hero with scrim + wave -->
<header class="relative isolate overflow-hidden text-white px-6 pt-21 pb-[clamp(96px,13vw,150px)] text-center before:content-[''] before:absolute before:inset-0 before:-z-10 before:bg-[linear-gradient(180deg,oklch(0.15_0.03_260/0.3),oklch(0.13_0.03_260/0.82))]">
<img src="https://picsum.photos/seed/twd13a/1600/900" width="1600" height="900" alt="" fetchpriority="high" decoding="async" class="absolute inset-0 -z-20 w-full h-full object-cover object-[50%_35%]">
<div class="relative max-w-[46ch] mx-auto">
<p class="text-xs tracking-[0.2em] uppercase text-twd13-eyebrow mb-3">Full-bleed imagery</p>
<h2 class="text-[clamp(27px,4.6vw,46px)] leading-[1.06] font-semibold mb-2.5 [text-shadow:0_2px_18px_oklch(0.15_0.03_260/0.55)]">Legible over any photo</h2>
<p class="text-[15px]">Gradient scrim below, wave on top, text in the safe zone.</p>
</div>
<svg class="absolute inset-x-0 -bottom-px z-10 block w-full h-[clamp(56px,9vw,116px)] text-twd13-next [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">
<path d="M0,62 C280,118 560,10 840,54 C1080,92 1250,106 1440,74 L1440,120 L0,120 Z"></path>
</svg>
</header>
<div class="bg-twd13-next text-twd13-nextink px-6 pt-6 pb-15 text-center">
<p class="text-[15px]">The wave fill matches this section exactly — no sticker effect.</p>
</div>
<!-- Wave-masked image band -->
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px]">Text section</p>
</div>
<div role="img" aria-label="Coastline photograph shown inside a wave-shaped band" class="-my-6 h-[clamp(220px,32vw,380px)] bg-twd13-bandfallback bg-[url('https://picsum.photos/seed/twd13b/1600/700')] bg-cover bg-center bg-no-repeat [mask:var(--twd13-band)_no-repeat_center/100%_100%]"></div>
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px] max-w-[52ch] mx-auto">The photo itself is masked into a wave band — both edges curved, no SVG node.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-twd13-next: #f6f7fa;
--color-twd13-next: oklch(0.97 0.01 250);
--color-twd13-nextink: #3f4655;
--color-twd13-nextink: oklch(0.42 0.03 250);
--color-twd13-eyebrow: #dfe4ee;
--color-twd13-eyebrow: oklch(0.91 0.01 250);
--color-twd13-paper: #f3f5f8;
--color-twd13-paper: oklch(0.96 0.01 230);
--color-twd13-paperink: #414a55;
--color-twd13-paperink: oklch(0.43 0.03 230);
--color-twd13-bandfallback: #35657f;
--color-twd13-bandfallback: oklch(0.45 0.07 230);
--font-twd13-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The ribbon cutter: a wave on the top AND bottom edge of one mask. */
.twd-13 {
width: 100%;
min-height: 100svh;
--twd13-band: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20400'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,70%20C360,-10%20700,120%201000,70%20C1180,40%201300,40%201440,66%20L1440,330%20C1200,410%20900,270%20620,326%20C420,366%20180,366%200,334%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the photo band. */
.twd-13 [role="img"] {
-webkit-mask: var(--twd13-band) no-repeat center/100% 100%;
}
```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 Wave Section Divider with an Overlay Background Image
Source: https://codefronts.com/layouts/tailwind-css-wave-section-dividers/tailwind-wave-section-divider-overlay-background-image/
A crisp wave over a full-bleed photo, with the contrast scrim, focal-point crop and LCP hygiene handled properly — the combination that usually goes wrong when a divider is dropped onto imagery. The second variant masks the photo itself into a wave-edged ribbon.
## HTML
```html
<section class="twd-13 w-full min-h-svh flex flex-col bg-twd13-paper font-twd13-sans">
<!-- Photo hero with scrim + wave -->
<header class="relative isolate overflow-hidden text-white px-6 pt-21 pb-[clamp(96px,13vw,150px)] text-center before:content-[''] before:absolute before:inset-0 before:-z-10 before:bg-[linear-gradient(180deg,oklch(0.15_0.03_260/0.3),oklch(0.13_0.03_260/0.82))]">
<img src="https://picsum.photos/seed/twd13a/1600/900" width="1600" height="900" alt="" fetchpriority="high" decoding="async" class="absolute inset-0 -z-20 w-full h-full object-cover object-[50%_35%]">
<div class="relative max-w-[46ch] mx-auto">
<p class="text-xs tracking-[0.2em] uppercase text-twd13-eyebrow mb-3">Full-bleed imagery</p>
<h2 class="text-[clamp(27px,4.6vw,46px)] leading-[1.06] font-semibold mb-2.5 [text-shadow:0_2px_18px_oklch(0.15_0.03_260/0.55)]">Legible over any photo</h2>
<p class="text-[15px]">Gradient scrim below, wave on top, text in the safe zone.</p>
</div>
<svg class="absolute inset-x-0 -bottom-px z-10 block w-full h-[clamp(56px,9vw,116px)] text-twd13-next [&_path]:fill-current pointer-events-none" viewBox="0 0 1440 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">
<path d="M0,62 C280,118 560,10 840,54 C1080,92 1250,106 1440,74 L1440,120 L0,120 Z"></path>
</svg>
</header>
<div class="bg-twd13-next text-twd13-nextink px-6 pt-6 pb-15 text-center">
<p class="text-[15px]">The wave fill matches this section exactly — no sticker effect.</p>
</div>
<!-- Wave-masked image band -->
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px]">Text section</p>
</div>
<div role="img" aria-label="Coastline photograph shown inside a wave-shaped band" class="-my-6 h-[clamp(220px,32vw,380px)] bg-twd13-bandfallback bg-[url('https://picsum.photos/seed/twd13b/1600/700')] bg-cover bg-center bg-no-repeat [mask:var(--twd13-band)_no-repeat_center/100%_100%]"></div>
<div class="bg-twd13-paper text-twd13-paperink px-6 py-10 text-center">
<p class="text-[15px] max-w-[52ch] mx-auto">The photo itself is masked into a wave band — both edges curved, no SVG node.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-twd13-next: #f6f7fa;
--color-twd13-next: oklch(0.97 0.01 250);
--color-twd13-nextink: #3f4655;
--color-twd13-nextink: oklch(0.42 0.03 250);
--color-twd13-eyebrow: #dfe4ee;
--color-twd13-eyebrow: oklch(0.91 0.01 250);
--color-twd13-paper: #f3f5f8;
--color-twd13-paper: oklch(0.96 0.01 230);
--color-twd13-paperink: #414a55;
--color-twd13-paperink: oklch(0.43 0.03 230);
--color-twd13-bandfallback: #35657f;
--color-twd13-bandfallback: oklch(0.45 0.07 230);
--font-twd13-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The ribbon cutter: a wave on the top AND bottom edge of one mask. */
.twd-13 {
width: 100%;
min-height: 100svh;
--twd13-band: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201440%20400'%20preserveAspectRatio='none'%3E%3Cpath%20d='M0,70%20C360,-10%20700,120%201000,70%20C1180,40%201300,40%201440,66%20L1440,330%20C1200,410%20900,270%20620,326%20C420,366%20180,366%200,334%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the photo band. */
.twd-13 [role="img"] {
-webkit-mask: var(--twd13-band) no-repeat center/100% 100%;
}
```How this works
Three layers, in order: the image (object-cover with a focal object-position), a gradient scrim for contrast, then the wave on top filled with the next section's colour.
class="relative isolate overflow-hidden
before:absolute before:inset-0 before:-z-10
before:bg-[linear-gradient(180deg,#0a0e14/0.28,#0a0e14/0.8)]"The scrim being a gradient is the whole trick: density where the copy and the wave live, transparency where the photograph is doing the work. The wave then reads as the page's edge rather than a sticker on the photo.
Variant b inverts the relationship — the photo is masked into a band with a wave on both edges, so the imagery itself is the divider.
Make it yours
- Change the scrim stops to suit a bright or a dark photo; the bottom stop is the one that protects the type.
- Move
object-[50%_35%]to keep a subject's face clear of the curve. - Add a duotone layer with
mix-blend-colorfor a branded photo treatment. - Swap the
<img>for a muted looping<video>— the layer order is identical.
Gotchas — read before shipping
- Test the headline against the darkest AND lightest parts of the photo, not the average. A scrim tuned to the average fails on the sky.
- Decorative photos take
alt=""; a meaningful one needs real alt text. The wave is always decorative. - Always give the image intrinsic
width/heightor the hero shifts when it lands — the CLS this whole collection avoids elsewhere. - Never put copy in the wave's deepest 40px; it gets clipped on small screens where the curve is proportionally taller.
isolateis required with negative z-index children. Without it,-z-20puts the image behind the page background.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
object-fit and isolation are long-standing Baseline features; the masked-band variant needs mask-image (Safari 15.4+ with the shim), and falls back to a straight-edged photo band.