コンテナからの解放

今年もCSSには大変お世話になりました。なかでも今年になってとくにお世話になる頻度が増えたのが「calc()関数」。プロパティの値に、計算式を使わせてくれるスゴいやつです。ここでは、そんなcalc()関数を使った便利ワザについて書いてゆきます。
この記事は「今年お世話になったCSS Advent Calendar 2016」の23日めの記事です:)。
※2023年の11月に加筆・修正しています。
calc()関数を使った便利なスタイル
例えば下図みたいな、「サイト全体のコンテンツは横幅800pxで画面中央にレイアウトされていて、その中のあるセクションだけウィンドウ全幅まで広がってる」なんてデザインの場合。以前は、「サイト全体を包括するコンテナには横幅を指定しないで、セクションごとに横幅を指定」していたと思うんです。

HTMLとCSSは以下みたいな感じ。
main要素には何もスタイルを指定しないで、横幅800pxに固定する要素(広がらない要素)にだけ、横幅をキメたスタイルを指定しています。
<main>
<section class="container">
<h2>広がらないセクション</h2>
<p>そのころわたくしは、モリーオ市の博物局に勤めて居りました。<br>十八等官でしたから役所のなかでも、ずうっと下の方でしたし俸給もほんのわずかでしたが、受持ちが標本の採集や整理で生れ付き好きなことでしたから、わたくしは毎日ずいぶん愉快にはたらきました。</p>
</section>
<section>
<h2>広がるセクション</h2>
<p>殊にそのころ、モリーオ市では競馬場を植物園に拵え直すというので、その景色のいいまわりにアカシヤを植え込んだ広い地面が、切符売場や信号所の建物のついたまま、わたくしどもの役所の方へまわって来たものですから、わたくしはすぐ宿直という名前で月賦で買った小さな蓄音器と二十枚ばかりのレコードをもって、その番小屋にひとり住むことになりました。</p>
</section>
⋮
</main>
.container {
max-width: 800px;
margin: 0 auto;
}
.bg {
color: white;
background: var(--themecolor);
}
全幅にするコンテンツは、わかりやすいように背景色をつけています。
下記記事を見て、世界は一変しました。
Breaking Out With Viewport Units and Calc(2016.5.26)
こちらの記事の中で紹介されている方法はとってもシンプルで、calc()
を使った、下ソースコードのようなスタイルを用意しています。この計算式を使うことで、main要素の横幅を800pxに固定しちゃっても、その子要素を全幅に広げることができるんですよー:D!
.l-main {
max-width: 800px;
margin: 0 auto;
}
.breaking-out {
margin-inline: calc(50% - 50vw);
}
<main class="l-main">
<h1>calc()とvwを使ったコンテナからの解放。</h1>
<section>
<h2>広がらないセクション</h2>
<p>そのころわたくしは、モリーオ市の博物局に勤めて居りました。<br>十八等官でしたから役所のなかでも、ずうっと下の方でしたし俸給もほんのわずかでしたが、受持ちが標本の採集や整理で生れ付き好きなことでしたから、わたくしは毎日ずいぶん愉快にはたらきました。</p>
</section>
<section class="breaking-out">
<h2>広がるセクション</h2>
<p>殊にそのころ、モリーオ市では競馬場を植物園に拵え直すというので、その景色のいいまわりにアカシヤを植え込んだ広い地面が、切符売場や信号所の建物のついたまま、わたくしどもの役所の方へまわって来たものですから、わたくしはすぐ宿直という名前で月賦で買った小さな蓄音器と二十枚ばかりのレコードをもって、その番小屋にひとり住むことになりました。</p>
</section>
⋮
</main>
「広がらないセクションにclassを付与する」よりも、「広がるセクションにclassを付与する」方が、HTML的にもなんだかわかりやすくなった気がします:D。
コンテナからの解放
このスタイルが何をしているのか詳しく見てゆきます。と言ってもとっても明快です。
.breaking-out {
margin-inline: calc(50% - 50vw);
}
異なる2種類の単位が使われてます。
ひとつめの「%
」は、親要素を基準とした割合を表す単位。ふたつめの「vw
」は、ビューポート(ウィンドウ)の横幅を基準とした割合を表す単位。その計算結果を、左右のマージンに適用しています。小さな値から大きな値を引くことになるので、左右にはネガティブマージンが適用されることになります。
「要素の横幅の半分」から「ウィンドウの横幅の半分」を引いた分ずつ。ちょうど、section要素とウィンドウ端までの狭間を埋めるように、要素を左右に引っ張ってるわけですねー明快:D!

全幅に広がるセクション
このセクションに適用してみました! ご覧の通り、section要素が横幅いっぱいに広がってますね!
セクションの中身だけ元の横幅に戻すには、さらに次のセクションのように指定します。
<section class="breaking-out">
<h2>全幅に広がるセクション</h2>
<p>このセクションに適用してみました! ご覧の通り、section要素は横幅いっぱいに広がってますね!<br>
セクションの中身だけ<em>元の横幅</em>に戻すには、さらに次のセクションのように指定します。</p>
</section>
中身だけ戻すセクション
ネガティブマージンで左右に広げた分と同じだけ内側に戻せばいいので、今度は逆に「ウィンドウの横幅の半分(50vw
)」から「要素の横幅の半分(50%
)」を引いた分ずつ、左右にパディングとして指定します。
.breaking-out.-bg {
padding-inline: calc(50vw - 50%);
}
<section class="breaking-out -bg">
<h2 class="c-title">中身だけ戻すセクション</h2>
<p>ネガティブマージンで左右に広げた分と同じだけ<b>内側</b>に戻すってことなので、今度は逆に「ウィンドウの横幅の半分」から「要素の横幅の半分」を引いた分ずつ、<strong>左右にパディング</strong>として指定します。</p>
</section>

%
は親要素が基準というとこがポイント。スクロールバー問題
冒頭で紹介した記事によると、この方法だと、スクロールバーが見える状態の場合*に、水平スクロールバーが表示されるようになっちゃうとのこと。vw
単位は、スクロールバーの領域も含めた横幅なので、垂直スクロールバーが表示されてると、スクロールバーの幅分誤差が出ちゃうんですね。
なので、html要素とbody要素に対して、X方向のみスクロールバーが出ないよう指定しておくと良いです。
html, body {
overflow-x: hidden;
}
大抵のブラウザではhtml要素かbody要素どちらか片方に指定すれば問題ないですが、古いiOSでは、どちらか片方だとまだ横にスクロールできちゃうので、念の為、両方に指定しておくとよいです。
けれども、body要素にoverflow: hiddenを指定していると、position: stickyを使いたい時に困っちゃいそうで心配ですね…。
以下に、スクロールバー問題の解決策をふたつご紹介します。
CSS変数を使った解決策
横スクロールバーが表示されちゃうということは、左右に広げるためのネガティブマージンが余分に適用されちゃってるということ。ネガティブマージンの値を、スクロールバーの幅分少なくすれば良いわけです;)。
そのために、CSS変数(カスタムプロパティ)と、JavaScriptを使います。
CSS変数(カスタムプロパティ)について、詳しくは下記記事を参照のこと。
変数にすると良い値 - CSS変数の使い方。
カスタムプロパティ
CSSに、スクロールバーの幅を設定するための--scrollbarWidth
という名前のカスタムプロパティを用意します。ひとまずスクロールバーが表示されてない時の状態を想定して、0px
という値を設定しておきます。
:root {
--scrollbarWidth: 0px;
}
JavaScript
JavaScriptには、スクロールバーが表示されているかどうかの判別と、本当のスクロールバーの幅を取得するための関数を用意します。
function scrollbarCheck() {
const doc = document.documentElement; // <- つまり <html> のこと
if (window.innerWidth !== doc.clientWidth) {
doc.classList.add('has-scrollbar');
doc.style.setProperty('--scrollbarWidth', `${window.innerWidth - doc.clientWidth}px`);
} else {
doc.classList.remove('has-scrollbar');
doc.style.setProperty('--scrollbarWidth', '0px');
}
}
innerWidthはスクロールバーを含むビューポートの横幅を取得して、clientWidthでスクロールバーを除くビューポートの横幅を取得するので、もしそのふたつの幅に違いがあれば、スクロールバーがあるってことになるってわけ;)。
もしスクロールバーが表示あったら、html要素にhas-scrollbar
というclass名を付けて、前述で用意したカスタムプロパティ--scrollbarWidth
の値を、そのふたつの差で上書きします。
これを、ページが読み込まれた時と、ウィンドウをリサイズした時に実行するようにすれば、下準備OK!;)
document.addEventListener("DOMContentLoaded", () => {
scrollbarCheck();
});
window.addEventListener('resize', () => {
scrollbarCheck();
});
ネガティブマージンを再計算
スクロールバーがある時、ウィンドウ幅は100vw
からスクロールバーの幅を引いた値になります。左右のネガティブマージンの計算も、50% - 50vw
だと、スクロールバーの幅分引き過ぎになるので、それぞれにスクロールバーの幅の半分を足せば良いってこと。つまり下ソースコードのようになります。
.has-scrollbar .breaking-out {
margin-inline: calc(50% - 50vw + var(--scrollbarWidth) / 2);
}

内側に戻すためのパディングには、逆に、スクロールバーの幅の半分を引けば良いですね。;D
.has-scrollbar .breaking-out.-bg {
padding-inline: calc(50vw - 50% - var(--scrollbarWidth) / 2);
}
コンテナクエリを使った解決策
前述の策では、スクロールバーを除くウィンドウの横幅を算出するために、「スクロールバーを含むウィンドウの横幅」から「スクロールバーの幅」を引いていたけれど、それはvw
のサイズが、スクロールバーも含んでしまっていたから。そもそもスクロールバーを除いたウィンドウ幅を指定できれば、そんな面倒くさい計算しなくて済むのになぁ…。そんな時には、コンテナクエリで使える新しい単位、cqi
*を使えばもっとシンプルに解決できます!:D
*Container Queries Inline
コンテナクエリ
コンテナクエリとは、特定の要素のサイズに応じてスタイルを適用し分けることができる、CSSの新しい仕様です。
コンテナクエリを利用するには、まず、基盤となる要素にcontainer-typeプロパティを指定します。
コンテナタイプを指定すると、その子要素では、cqi
というコンテナ単位が使えるようになります。
コンテナ単位100cqi
とは、コンテナタイプを指定した要素(以下「その要素」という)の横幅100%
のこと。50cqi
だと、その要素の横幅の50%
、1cqi
だと、その要素の横幅の1%
を表します。
コンテナクエリについて詳しくは、こちらのページを参照のこと。
CSS コンテナ クエリ - CSS: カスケード スタイル シート | MDN
コンテナタイプをbody要素に指定すれば、50cqi
で、body要素の横幅の半分、それ即ちスクロールバーを除いたウィンドウの横幅の半分が適用できるってわけですねー:D!
となれば話は早い!
body要素にコンテナタイプを指定して、上のサンプルで50vw
としていたところを50cqi
に書き換えれば、一件落着!
body {
container-type: inline-size;
}
.breaking-out {
margin-inline: calc(50% - 50cqi);
}
.breaking-out.-bg {
padding-inline: calc(50cqi - 50%);
}
さいごに
calc()関数には他にも、等分割で横並びの時の余白とか、アイコンを要素の上下中央に絶対配置する時とか、いろんな場面でいろんな面倒が端折れて大変重宝しました。
2017年も引き続きお世話になると思います:)。
以上、_watercolorの、「今年お世話になったCSS Advent Calendar 2016」でした!
2023年以降も引き続き、コンテナクエリと組み合わせてどんどん便利;D!
ひーろがーれよ♪ʺପ(๑бωб)੭。◌⑅⃝*॰ॱ