CSSで波の形の区切り線を作る方法
区画と区画の境目を波の形にするには、波の形に塗った SVG を、2つの区画の間に横幅いっぱいで置きます。SVG には display: block と preserveAspectRatio="none" を付けます。display: block がないと波の下に細いすき間ができ、preserveAspectRatio="none" がないと画面の幅によって波が縮んで、どちらも上の区画の色がのぞきます。SVG を使わずに CSS だけで描きたいときは、2つの radial-gradient() で半円を交互に並べます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、区切りの要素のスクリーンショットの画素を読んで、色が変わる位置を調べました。Firefox と Safari、スマートフォンの実機、波を動かすアニメーションは試していません。
波の形の SVG を、2つの区画の間に置く
上の区画を水色(#00c8e8)、下の区画を白にする例です。区切りの要素の背景を上の区画と同じ水色にして、その中の SVG で、波より下を下の区画の色(白)で塗ります。
<section class="hero">…</section>
<div class="wave">
<svg viewBox="0 0 1200 120" preserveAspectRatio="none" aria-hidden="true">
<path d="M0,60 Q150,0 300,60 T600,60 T900,60 T1200,60 V120 H0 Z" fill="#fff" />
</svg>
</div>
<section class="content">…</section>
.hero { background: #00c8e8; }
.content { background: #fff; }
.wave { background: #00c8e8; }
.wave svg {
display: block; /* 下にすき間ができないようにする */
width: 100%;
height: 80px;
}
path の d は、左端の高さ 60 から始めて、2次ベジェ曲線の Q と、それを続けて描く T で山と谷を2つずつ描きます。T は前の曲線の制御点をもとに次の制御点を決めるので、終点を書き足すだけで波が続きます。最後の V120 H0 Z で右下と左下を通って閉じ、波より下を塗りつぶします。
区切りは見た目のためだけの図形なので、aria-hidden="true" を付けて、スクリーンリーダーなどに読み上げられないようにしています。
波の下にすき間ができるときは display: block を付ける
HTML の中に書いた SVG は、何も指定しないとインラインの要素として、文字と同じように行の中に置かれます。vertical-align の初期値は baseline で、MDN によると、ベースラインを持たない要素ではマージンの下端がベースラインにそろいます。検証環境で測ると、SVG の下に余白が残っていました。

幅 1280px のページで測ったところ、display: block がないと、高さ 80px の SVG を入れた区切りの要素の高さが 85px になりました。SVG の下に 5px の水色の帯が残り、下の白い区画との間に線が入ったように見えます。display: block を付けると要素の高さは 80px になり、いちばん下の行の色は白(#ffffff)でした。
画面の幅で波が縮むときは preserveAspectRatio="none" にする
preserveAspectRatio を書かないと、既定の xMidYMid meet になります。meet は縦横の比率を保ったまま、viewBox 全体が収まる大きさに拡大・縮小するので、SVG の枠と viewBox の比率が違うと、枠の中に何も描かれない部分ができます。

viewBox が 1200×120 の SVG を、幅 100%・高さ 80px で置いて測りました。
| 画面の幅 | 既定(meet) | none |
|---|---|---|
| 1280px | 左右の端の 240px ずつに波が描かれず、いちばん下の行の 480px が水色 | いちばん下の行に水色の画素はない |
| 375px | 波が縮み、下の 21 行が水色の帯になる | いちばん下の行に水色の画素はない |
幅の広い画面では左右が、狭い画面では下側が空き、どちらも上の区画の水色が見えてしまいます。MDN の説明によると、none を指定すると、縦横の比率を保たずに、viewBox が SVG の枠にぴったり合うように伸び縮みします。上の画像の下の段のように、幅 360px でも波は高さいっぱいに描かれ、下の白い区画につながりました。
SVG を使わずに、radial-gradient を2つ重ねて描く
HTML に SVG を書きたくないときは、radial-gradient() で円を描き、background-repeat の repeat-x で横に並べても波が作れます。上に出る半円(山)と、下にへこむ半円(谷)を交互に並べる形です。
<section class="hero">…</section>
<div class="css-wave"></div>
<section class="content">…</section>
.css-wave {
--r: 20px; /* 半円の半径。山の高さと谷の深さが --r になる */
--c: #fff; /* 下の区画の色 */
height: calc(3 * var(--r));
background:
/* 山:中心線(上から --r の位置)の上に出る円 */
radial-gradient(circle at 50% 50%, var(--c) var(--r), #0000 calc(var(--r) + 0.5px))
0 0 / calc(4 * var(--r)) calc(2 * var(--r)) repeat-x,
/* 谷:中心線より下を塗り、円の形にくりぬく */
radial-gradient(circle at 50% 0, #0000 var(--r), var(--c) calc(var(--r) + 0.5px))
calc(2 * var(--r)) var(--r) / calc(4 * var(--r)) calc(2 * var(--r)) repeat-x,
#00c8e8; /* 上の区画の色 */
}
background の一括指定では、カンマで区切った層ごとに、画像の後ろに「位置 / 大きさ」と繰り返し方を書けます。1つ目のグラデーションは、幅 4 × --r・高さ 2 × --r の範囲の真ん中に、半径 --r の白い円を描きます。これを横に並べると、上の端から円が並びます。2つ目は、上から --r 下がった位置から始まる同じ大きさの範囲で、上の辺の真ん中を中心に半径 --r の円を透明にくりぬき、まわりを白で塗ります。こちらは横に 2 × --r ずらしてあるので、くぼみは山と山の間に来ます。
色が切り替わる位置を --r と --r + 0.5px に分けているのは、円のふちに中間の色を入れてなめらかにするためです。山の円1つ分(幅 80px)の範囲で、白にも水色にも近くない画素を数えると、0.5px ずらしたときは 32 個、同じ位置にしたときは 0 個でした。同じ位置にすると、ふちに中間の色の画素がほとんど入りません。

山の円の下半分は、谷のグラデーションの白に重なって見えなくなり、上半分だけが山として残ります。--r: 20px で測ったところ、区切りの要素の高さは 60px で、山の頂上(左から 40px の列)は上の端から白、谷の底(左から 80px の列)は上から 40px の位置から白でした。いちばん下の行に水色の画素はなく、下の区画とすき間なくつながります。
SVG を preserveAspectRatio="none" で伸ばすやり方とは違い、こちらは波1つの幅が 4 × --r(この例では 80px)に決まっていて、画面の幅が広いほど波の数が増えます。上から 10px の高さの行で白い山の数を数えると、幅 1280px では 16 個、幅 375px では 5 個でした。画面の幅にかかわらず波の数を一定にしたいなら SVG を、波の大きさを一定にしたいなら radial-gradient() を選びます。





