CSSで波の形の区切り線を作る方法

​​CSS​HTML,Playwright,SVG

区画と区画の境目を波の形にするには、波の形に塗った 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 で、波より下を下の区画の色(白)で塗ります。

html
<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>
css
.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 の下に余白が残っていました。

上下2段の比較。上の「display: block なし」は、白い波の下に水色の細い帯が横いっぱいに残り、ピンクの枠で「すき間」と示している。下の「display: block あり」は、白い波がそのまま下の白い区画につながっている
display: block がないと、波の下に上の区画の色が細い帯になって残る(上)。付けると帯は消える(下)

幅 1280px のページで測ったところ、display: block がないと、高さ 80px の SVG を入れた区切りの要素の高さが 85px になりました。SVG の下に 5px の水色の帯が残り、下の白い区画との間に線が入ったように見えます。display: block を付けると要素の高さは 80px になり、いちばん下の行の色は白(#ffffff)でした。

画面の幅で波が縮むときは preserveAspectRatio="none" にする

preserveAspectRatio を書かないと、既定の xMidYMid meet になります。meet は縦横の比率を保ったまま、viewBox 全体が収まる大きさに拡大・縮小するので、SVG の枠と viewBox の比率が違うと、枠の中に何も描かれない部分ができます。

上下2段の比較。幅 360px のページで、上の「preserveAspectRatio なし(既定の meet)」は、波が低く平たくなり、波の下に水色の太い帯が残っている。下の「preserveAspectRatio="none"」は、波が高さいっぱいに描かれ、下の白い区画につながっている
既定の meet では、狭い画面で波が縮み、下に水色の帯が残る(上)。none にすると、高さいっぱいに伸びる(下)

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 で横に並べても波が作れます。上に出る半円(山)と、下にへこむ半円(谷)を交互に並べる形です。

html
<section class="hero">…</section>
<div class="css-wave"></div>
<section class="content">…</section>
css
.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 個でした。同じ位置にすると、ふちに中間の色の画素がほとんど入りません。

3段の比較。上の「山の radial-gradient だけ」は、水色の上に白い円が横に並んでいる。中の「谷の radial-gradient だけ」は、中ほどから下が白で、そこに水色の半円が下向きに並んでいる。下の「2つを重ねたもの」は、白い半円の山と水色の半円の谷が交互に続く波になっている
山の円(上)と、谷をくりぬいた白い帯(中)を重ねると、波になる(下)

山の円の下半分は、谷のグラデーションの白に重なって見えなくなり、上半分だけが山として残ります。--r: 20px で測ったところ、区切りの要素の高さは 60px で、山の頂上(左から 40px の列)は上の端から白、谷の底(左から 80px の列)は上から 40px の位置から白でした。いちばん下の行に水色の画素はなく、下の区画とすき間なくつながります。

SVG を preserveAspectRatio="none" で伸ばすやり方とは違い、こちらは波1つの幅が 4 × --r(この例では 80px)に決まっていて、画面の幅が広いほど波の数が増えます。上から 10px の高さの行で白い山の数を数えると、幅 1280px では 16 個、幅 375px では 5 個でした。画面の幅にかかわらず波の数を一定にしたいなら SVG を、波の大きさを一定にしたいなら radial-gradient() を選びます。

​Posted by 86