Pure TailwindMIT licensed

Tailwind Vertical Wave Divider for Side Navigation

The wave rotated 90°: a curved vertical seam between a sidebar and the content column. Includes the container-query collapse — @max-md: on a @container root — that turns the vertical wave into a horizontal one when the layout stacks.

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-14 w-full min-h-svh flex flex-col justify-center gap-0 font-twd14-sans">

  <!-- Sidebar with a curved vertical seam -->
  <div class="@container w-full">
    <div class="grid grid-cols-[minmax(190px,28%)_1fr] @max-[560px]:grid-cols-1 min-h-[320px] bg-twd14-main">
      <aside class="relative bg-twd14-side text-white pl-7 pr-6 py-8 @max-[560px]:pb-14">
        <p class="text-base font-extrabold tracking-[-0.01em] mb-6">codefronts</p>
        <nav aria-label="Section navigation">
          <ul class="list-none flex flex-col gap-1 pr-9 @max-[560px]:pr-0">
            <li><a href="#0" aria-current="page" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white aria-[current=page]:bg-twd14-active aria-[current=page]:text-white aria-[current=page]:shadow-[inset_2px_0_0_var(--color-twd14-accent)] focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Overview</a></li>
            <li><a href="#0" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Dividers</a></li>
            <li><a href="#0" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Gradients</a></li>
            <li><a href="#0" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Masks</a></li>
          </ul>
        </nav>
        <svg class="absolute inset-y-0 -right-px block h-full w-[clamp(26px,4vw,58px)] text-twd14-main [&_path]:fill-current pointer-events-none @max-[560px]:inset-y-auto @max-[560px]:inset-x-0 @max-[560px]:-bottom-px @max-[560px]:h-[34px] @max-[560px]:w-full" viewBox="0 0 120 800" preserveAspectRatio="none" aria-hidden="true" focusable="false">
          <path d="M64,0 C104,140 24,260 60,400 C96,540 20,660 56,800 L120,800 L120,0 Z"></path>
        </svg>
      </aside>
      <main class="px-8 pt-9 pb-10 text-twd14-mainink">
        <h2 class="text-[clamp(21px,3vw,30px)] font-semibold mb-2.5">Vertical seam, authored portrait</h2>
        <p class="text-[15px] max-w-[48ch]">The wave runs down the grid instead of across it &mdash; for split-screen and creative sidebar layouts. Narrow the pane and the container query re-pins it to the bottom edge.</p>
      </main>
    </div>
  </div>

  <!-- Split-screen vertical mask -->
  <div class="@container w-full">
    <div class="grid grid-cols-2 @max-[620px]:grid-cols-1 min-h-[300px] bg-twd14-left">
      <div class="flex flex-col justify-center gap-2 px-7 py-10 text-twd14-onleft">
        <h2 class="text-[clamp(22px,3.2vw,32px)] font-semibold">Split screen</h2>
        <p class="text-[15px] max-w-[36ch]">Masked edge, no SVG node.</p>
      </div>
      <div class="twd-14-seam flex items-center -ml-8 pl-32 pr-7 py-10 bg-twd14-right text-twd14-rightink [mask:var(--twd14-seam)_no-repeat_left_center/120px_100%,linear-gradient(#000,#000)_no-repeat_right_center/calc(100%-118px)_100%] @max-[620px]:ml-0 @max-[620px]:pl-7 @max-[620px]:[mask:none]">
        <p class="text-[15px]">The right panel carries a vertical wave mask on its left edge, so the two halves interlock.</p>
      </div>
    </div>
  </div>

</section>
@import "tailwindcss";

@theme {
  --color-twd14-side: #2b2547;
  --color-twd14-side: oklch(0.26 0.06 285);
  --color-twd14-onside: #ddd8ec;
  --color-twd14-onside: oklch(0.89 0.03 285);
  --color-twd14-hover: #3c3560;
  --color-twd14-hover: oklch(0.34 0.06 285);
  --color-twd14-active: #473c73;
  --color-twd14-active: oklch(0.38 0.08 285);
  --color-twd14-accent: #e3c14a;
  --color-twd14-accent: oklch(0.83 0.14 95);
  --color-twd14-main: #f5f4f9;
  --color-twd14-main: oklch(0.96 0.01 285);
  --color-twd14-mainink: #3f3a55;
  --color-twd14-mainink: oklch(0.4 0.04 285);
  --color-twd14-left: #4a221f;
  --color-twd14-left: oklch(0.3 0.08 25);
  --color-twd14-onleft: #f6e7db;
  --color-twd14-onleft: oklch(0.94 0.03 60);
  --color-twd14-right: #f6efe3;
  --color-twd14-right: oklch(0.95 0.03 70);
  --color-twd14-rightink: #54382a;
  --color-twd14-rightink: oklch(0.38 0.07 45);
  --font-twd14-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The portrait seam for the split-screen mask. It is masked at a FIXED 120px width and
   composited with an opaque layer for the rest of the panel, so the curve keeps its
   authored proportions instead of stretching across the whole half. */

.twd-14 {
  width: 100%;
  min-height: 100svh;
  --twd14-seam: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20200%20800'%20preserveAspectRatio='none'%3E%3Cpath%20d='M60,0%20C110,160%2020,300%2066,440%20C104,556%2026,660%2062,800%20L200,800%20L200,0%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the split-screen seam. */

@media (width >= 39rem) {
  .twd-14 .twd-14-seam {
    -webkit-mask: var(--twd14-seam) no-repeat left center/120px 100%, linear-gradient(#000, #000) no-repeat right center/calc(100% - 118px) 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-twd14-side: #2b2547;
  --color-twd14-side: oklch(0.26 0.06 285);
  --color-twd14-onside: #ddd8ec;
  --color-twd14-onside: oklch(0.89 0.03 285);
  --color-twd14-hover: #3c3560;
  --color-twd14-hover: oklch(0.34 0.06 285);
  --color-twd14-active: #473c73;
  --color-twd14-active: oklch(0.38 0.08 285);
  --color-twd14-accent: #e3c14a;
  --color-twd14-accent: oklch(0.83 0.14 95);
  --color-twd14-main: #f5f4f9;
  --color-twd14-main: oklch(0.96 0.01 285);
  --color-twd14-mainink: #3f3a55;
  --color-twd14-mainink: oklch(0.4 0.04 285);
  --color-twd14-left: #4a221f;
  --color-twd14-left: oklch(0.3 0.08 25);
  --color-twd14-onleft: #f6e7db;
  --color-twd14-onleft: oklch(0.94 0.03 60);
  --color-twd14-right: #f6efe3;
  --color-twd14-right: oklch(0.95 0.03 70);
  --color-twd14-rightink: #54382a;
  --color-twd14-rightink: oklch(0.38 0.07 45);
  --font-twd14-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The portrait seam for the split-screen mask. It is masked at a FIXED 120px width and
   composited with an opaque layer for the rest of the panel, so the curve keeps its
   authored proportions instead of stretching across the whole half. */

.twd-14 {
  width: 100%;
  min-height: 100svh;
  --twd14-seam: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20200%20800'%20preserveAspectRatio='none'%3E%3Cpath%20d='M60,0%20C110,160%2020,300%2066,440%20C104,556%2026,660%2062,800%20L200,800%20L200,0%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the split-screen seam. */

@media (width >= 39rem) {
  .twd-14 .twd-14-seam {
    -webkit-mask: var(--twd14-seam) no-repeat left center/120px 100%, linear-gradient(#000, #000) no-repeat right center/calc(100% - 118px) 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&display=swap">

What changed in the port

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

  • @container plus @max-[560px]: is the port's most consequential change: the CSS original used a container query too, but writing the collapse as variants on the same elements makes the two layouts readable as one design rather than two.
  • The wave's own re-pinning (@max-[560px]:inset-y-auto @max-[560px]:-bottom-px @max-[560px]:h-[34px] @max-[560px]:w-full) is verbose and worth it — the element that changes orientation carries the change.
  • aria-[current=page]: replaces a[aria-current="page"]. Tailwind's built-in aria-current: shorthand compiles to [aria-current="true"], which is never the right value for navigation — always write the explicit form.
  • UI change from the CSS original: the split-screen mask stretched one 200-unit-wide seam across the whole right half, so the curve's crest reached ~250px into the panel and clipped the first words of every line. It is now masked at a fixed 120px and composited with an opaque layer for the remainder, which keeps the seam's authored proportions and lets the copy start at a predictable 128px.
  • UI change from the CSS original: the nav links were padding:8px 10px (about 33px tall) with rgba(255,255,255,.82) ink. They are now min-h-11 at 44px with a token colour, and the @max-* variant drops the list's right padding when the wave moves to the bottom, which the original kept and wasted.
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 Vertical Wave Divider for Side Navigation
Source: https://codefronts.com/layouts/tailwind-css-wave-section-dividers/tailwind-vertical-wave-divider-side-navigation/

The wave rotated 90&deg;: a curved vertical seam between a sidebar and the content column. Includes the container-query collapse &mdash; @max-md: on a @container root &mdash; that turns the vertical wave into a horizontal one when the layout stacks.
## HTML
```html
<section class="twd-14 w-full min-h-svh flex flex-col justify-center gap-0 font-twd14-sans">

  <!-- Sidebar with a curved vertical seam -->
  <div class="@container w-full">
    <div class="grid grid-cols-[minmax(190px,28%)_1fr] @max-[560px]:grid-cols-1 min-h-[320px] bg-twd14-main">
      <aside class="relative bg-twd14-side text-white pl-7 pr-6 py-8 @max-[560px]:pb-14">
        <p class="text-base font-extrabold tracking-[-0.01em] mb-6">codefronts</p>
        <nav aria-label="Section navigation">
          <ul class="list-none flex flex-col gap-1 pr-9 @max-[560px]:pr-0">
            <li><a href="#0" aria-current="page" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white aria-[current=page]:bg-twd14-active aria-[current=page]:text-white aria-[current=page]:shadow-[inset_2px_0_0_var(--color-twd14-accent)] focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Overview</a></li>
            <li><a href="#0" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Dividers</a></li>
            <li><a href="#0" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Gradients</a></li>
            <li><a href="#0" class="flex items-center min-h-11 px-2.5 rounded-lg text-[14.5px] text-twd14-onside no-underline hover:bg-twd14-hover hover:text-white focus-visible:outline-2 focus-visible:outline-twd14-accent focus-visible:outline-offset-2">Masks</a></li>
          </ul>
        </nav>
        <svg class="absolute inset-y-0 -right-px block h-full w-[clamp(26px,4vw,58px)] text-twd14-main [&_path]:fill-current pointer-events-none @max-[560px]:inset-y-auto @max-[560px]:inset-x-0 @max-[560px]:-bottom-px @max-[560px]:h-[34px] @max-[560px]:w-full" viewBox="0 0 120 800" preserveAspectRatio="none" aria-hidden="true" focusable="false">
          <path d="M64,0 C104,140 24,260 60,400 C96,540 20,660 56,800 L120,800 L120,0 Z"></path>
        </svg>
      </aside>
      <main class="px-8 pt-9 pb-10 text-twd14-mainink">
        <h2 class="text-[clamp(21px,3vw,30px)] font-semibold mb-2.5">Vertical seam, authored portrait</h2>
        <p class="text-[15px] max-w-[48ch]">The wave runs down the grid instead of across it &mdash; for split-screen and creative sidebar layouts. Narrow the pane and the container query re-pins it to the bottom edge.</p>
      </main>
    </div>
  </div>

  <!-- Split-screen vertical mask -->
  <div class="@container w-full">
    <div class="grid grid-cols-2 @max-[620px]:grid-cols-1 min-h-[300px] bg-twd14-left">
      <div class="flex flex-col justify-center gap-2 px-7 py-10 text-twd14-onleft">
        <h2 class="text-[clamp(22px,3.2vw,32px)] font-semibold">Split screen</h2>
        <p class="text-[15px] max-w-[36ch]">Masked edge, no SVG node.</p>
      </div>
      <div class="twd-14-seam flex items-center -ml-8 pl-32 pr-7 py-10 bg-twd14-right text-twd14-rightink [mask:var(--twd14-seam)_no-repeat_left_center/120px_100%,linear-gradient(#000,#000)_no-repeat_right_center/calc(100%-118px)_100%] @max-[620px]:ml-0 @max-[620px]:pl-7 @max-[620px]:[mask:none]">
        <p class="text-[15px]">The right panel carries a vertical wave mask on its left edge, so the two halves interlock.</p>
      </div>
    </div>
  </div>

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

@theme {
  --color-twd14-side: #2b2547;
  --color-twd14-side: oklch(0.26 0.06 285);
  --color-twd14-onside: #ddd8ec;
  --color-twd14-onside: oklch(0.89 0.03 285);
  --color-twd14-hover: #3c3560;
  --color-twd14-hover: oklch(0.34 0.06 285);
  --color-twd14-active: #473c73;
  --color-twd14-active: oklch(0.38 0.08 285);
  --color-twd14-accent: #e3c14a;
  --color-twd14-accent: oklch(0.83 0.14 95);
  --color-twd14-main: #f5f4f9;
  --color-twd14-main: oklch(0.96 0.01 285);
  --color-twd14-mainink: #3f3a55;
  --color-twd14-mainink: oklch(0.4 0.04 285);
  --color-twd14-left: #4a221f;
  --color-twd14-left: oklch(0.3 0.08 25);
  --color-twd14-onleft: #f6e7db;
  --color-twd14-onleft: oklch(0.94 0.03 60);
  --color-twd14-right: #f6efe3;
  --color-twd14-right: oklch(0.95 0.03 70);
  --color-twd14-rightink: #54382a;
  --color-twd14-rightink: oklch(0.38 0.07 45);
  --font-twd14-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* The portrait seam for the split-screen mask. It is masked at a FIXED 120px width and
   composited with an opaque layer for the rest of the panel, so the curve keeps its
   authored proportions instead of stretching across the whole half. */

.twd-14 {
  width: 100%;
  min-height: 100svh;
  --twd14-seam: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20200%20800'%20preserveAspectRatio='none'%3E%3Cpath%20d='M60,0%20C110,160%2020,300%2066,440%20C104,556%2026,660%2062,800%20L200,800%20L200,0%20Z'%20fill='%23000'/%3E%3C/svg%3E");
}
/* Safari < 15.4 mask shim for the split-screen seam. */

@media (width >= 39rem) {
  .twd-14 .twd-14-seam {
    -webkit-mask: var(--twd14-seam) no-repeat left center/120px 100%, linear-gradient(#000, #000) no-repeat right center/calc(100% - 118px) 100%;
  }
}
```

How this works

A vertical wave is authored in a portrait viewBox and stretched down the seam with preserveAspectRatio="none", pinned with absolute inset-y-0 -right-px on the sidebar.

class="absolute inset-y-0 -right-px block h-full w-[clamp(26px,4vw,58px)]"

Do not rotate a landscape wave. rotate-90 does not rotate the box with it, and you will fight transform-origin forever. Author it portrait; it is the same amount of work once.

The collapse is a container query, not a media query. Below 560px of container width the grid becomes one column and the wave re-pins to the bottom edge as a horizontal strip — because a 28px-wide vertical wave on a phone reads as noise, not as a seam.

Make it yours

  • Sidebar width via the grid's minmax(); the wave scales with its own clamp().
  • Mirror it for a right-hand sidebar with -scale-x-100.
  • Use a stroked path instead of a fill for a thin decorative seam.
  • Give the active nav item its own small notch by reusing the same path at 20% scale.

Gotchas — read before shipping

  • Author portrait. Rotating a landscape viewBox is the single most common way this pattern goes wrong.
  • At narrow widths a vertical wave reads as noise — collapse it, which is what the container query is for.
  • Keep the nav a real <nav> with a list and aria-current; the wave is decoration only and must stay aria-hidden.
  • Give the list pr-* so the wave never overlaps a focus ring — a ring clipped by a decorative curve is a real accessibility bug.
  • @container queries need the parent to declare @container; without it the @max-* variants silently never match.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Container queries need Chrome 105+, Safari 16+, Firefox 110+ — comfortably inside Tailwind v4's own floor. The layout works everywhere; the query only makes the collapse smarter.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…