コンテナからの解放。

今年も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

「ウィンドウ幅からコンテンツ幅を引いて2で割った分」なのだから、考え方はとってもシンプル。

全幅に広がるセクション

このセクションに適用してみました! ご覧の通り、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に、スクロールバーを除くウィンドウ幅を設定するための--viewWidthという名前のカスタムプロパティを用意します。ひとまず100vwを指定しておきます。

:root {
	--viewWidth: 100vw;
}

JavaScript

JavaScriptには、スクロールバーが表示されているかどうかの判別と、スクロールバーを除くウィンドウ幅を取得するための関数を用意します。

function scrollbarCheck() {
	const doc = document.documentElement; // <- html要素 のこと
	if (window.innerWidth !== doc.clientWidth) {
		doc.classList.add('has-scrollbar');
	} else {
		doc.classList.remove('has-scrollbar');
	}
	doc.style.setProperty('--viewWidth', `${doc.clientWidth}px`);
}

window.innerWidthスクロールバーを含むウィンドウ幅を取得して、doc.clientWidthスクロールバーを除くウィンドウ幅を取得するので、もしそのふたつの幅に違いがあれば、スクロールバーがあるってことになるってわけ;)
もしスクロールバーがあったら、html要素has-scrollbarというclass名を付けて、前述で用意したカスタムプロパティ--viewWidthの値を、スクロールバーを除くウィンドウ幅で上書きします。

これを、ページが読み込まれた時と、ウィンドウをリサイズした時に実行するようにすれば、下準備OK!;)

document.addEventListener("DOMContentLoaded", () => {
	scrollbarCheck();
});
window.addEventListener('resize', () => {
	scrollbarCheck();
});

ネガティブマージンを再計算

スクロールバーがある時、ウィンドウ幅は100vwじゃなくて、JavaScriptで取得したスクロールバーを除くウィンドウ幅になります。50vwはその半分なので、スクロールバーがある時のネガティブマージンは、下ソースコードのようになります。

.has-scrollbar .breaking-out {
	margin-inline: calc(50% + var(--viewWidth) / 2);
}

内側に戻すためのパディングも同じように、50vwvar(--viewWidth) / 2に代えればよいですね;D

.has-scrollbar .breaking-out.-bg {
	padding-inline: calc(var(--viewWidth) / 2 - 50%);
}

コンテナクエリを使った解決策

前述の策では、JavaScriptでスクロールバーを除くウィンドウの横幅を取得していたけれど、そもそもスクロールバーを除いたウィンドウ幅を指定できれば、JavaScript使わなくても済むのになぁ…。そんな時には、コンテナクエリで使える新しい単位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

ひーろがーれよ♪ʺପ(๑бωб)੭。◌⑅⃝*॰ॱ