コンテナからの解放

今年もCSSには大変お世話になりました。なかでも今年になってとくにお世話になる頻度が増えたのが「calcキャルク()関数」。プロパティの値に、計算式を使わせてくれるスゴいやつです。ここでは、そんなcalc()関数を使った便利ワザについて書いてゆきます。
この記事は「今年お世話になったCSS Advent Calendar 2016」の23日めの記事です:)
※2022年の11月に加筆・修正しています。

calc()関数を使った便利なスタイル

例えば、下図にみたいな「コンテンツが画面中央に配置されていて、サイト全体のコンテンツは横幅980px内に収まるんだけど、その中のあるセクションだけ背景色がウィンドウの端っこまで広がってる」なんてデザインの場合。以前は、「サイト全体を包括するコンテナには横幅を指定しないで、セクションごとに横幅を指定」していたと思うんです。

HTMLとCSSは以下のように、article要素には横幅を指定しないで、ウィンドウ幅まで広がらないsection要素にだけ、横幅をキメたスタイルを指定するカンジです。

<article>
	<h1 class="container">今年お世話になったCSS</h1>
	<p class="container">横幅980pxで中央配置のコンテンツ。</p>
	<section class="container">
		<h2>広がらないセクション</h2>
		<p>Lollipop icing chupa chups macaroon. Macaroon marshmallow candy sesame snaps...</p>
	</section>
	<section>
		<div class="container">
		<h2>横幅いっぱいまで広がるセクション</h2>
		<p>Cupcake bonbon apple pie pastry muffin muffin oat cake. Brownie macaroon...</p>
		</div>
	</section>
	<section class="container">
		<h2>広がらないセクション</h2>
		<p>Lollipop icing chupa chups macaroon. Macaroon marshmallow candy sesame snaps...</p>
	</section>
</article>
.container {
	width: 980px;
	margin: 0 auto;
}

下記記事を見て、世界は一変しました。

Breaking Out With Viewport Units and Calc(2016.5.26)

こちらの記事の中で紹介されている計算式はとっても単純で、calc()を使って、以下のようにスタイルを指定します。すると、HTMLにはウィンドウ幅まで広がるsection要素にだけ、スタイルを指定すればよくなるんです:D

.breaking-out {
	margin-right: calc(50% - 50vw);
	margin-left: calc(50% - 50vw);
}
<article class="container">
	<h1>今年お世話になったCSS</h1>
	<p>横幅980pxで中央配置のコンテンツ。</p>
	<section>
		<h2>広がらないセクション</h2>
		<p>Lollipop icing chupa chups macaroon. Macaroon marshmallow candy sesame snaps...</p>
	</section>
	<section class="breaking-out">
		<h2>横幅いっぱいまで広がるセクション</h2>
		<p>Cupcake bonbon apple pie pastry muffin muffin oat cake. Brownie macaroon...</p>
	</section>
	<section>
		<h2>広がらないセクション</h2>
		<p>Lollipop icing chupa chups macaroon. Macaroon marshmallow candy sesame snaps...</p>
	</section>
</article>

コンテナからの解放

このスタイルが何をしているのか詳しく見てゆきます。と言ってもとっても明快です。

.breaking-out {
	margin-right: calc(50% - 50vw);
	margin-left: calc(50% - 50vw);
}

異なる2種類の単位が使われてます。
ひとつめの「%」は、親要素を基準とした割合を表す単位。ふたつめの「vw」は、ビューポート(ウィンドウ)の横幅を基準とした割合を表す単位。その計算結果を、左右のマージンに適用しています。小さな値から大きな値を引くことになるので、左右にはネガティブマージンが適用されることになります。
「要素の横幅の半分」から「ウィンドウの横幅の半分」を引いた分ずつ。ちょうど、article要素とウィンドウ端までの狭間を埋めるように、要素を左右に引っ張ってるわけですねー明快:D

「ウィンドウ幅からコンテンツ幅を引いて2で割った分」なのだから、考え方はとってもシンプル。
<section class="breaking-out">
	<h2>ウィンドウ幅いっぱいまで広がるセクション</h2>
	<p>広がってるのが見やすいように背景に色をつけてみました。ご覧の通り、section要素は横幅いっぱいに広がりました!けれども、文章もなんもかも広がっちゃってちょっとみっともないですね…。</p>
</section>

ウィンドウ幅いっぱいまで広がるセクション

このセクションに適用してみました!ご覧の通り、section要素は横幅いっぱいに広がりました、けれども、文章もなんもかも広がっちゃってちょっとみっともないですね…。
セクションの中身だけ元の横幅に戻すには、さらに次のセクションのように指定します。

背景だけ広がるセクション

ネガティブマージンで左右に広げた分と同じだけ内側に戻すため、今度は逆に、ウィンドウの横幅の半分から要素の横幅の半分を引いた分ずつ、左右にパディングとして指定します。

.section-container {
	padding-right: calc(50vw - 50%);
	padding-left: calc(50vw - 50%);
}
<section class="breaking-out section-container">
	<h2>背景だけ広がるセクション</h2>
	<p>ネガティブマージンで左右に広げた分と同じだけ内側に戻すため、今度は逆に、ウィンドウの横幅の半分から要素の横幅の半分を引いた分ずつ、左右にパディングとして指定します。</p>
</section>
セクションの幅は広がれど、%は親要素が基準というとこがポイント。

特定の要素だけ広げる

他にも、特定の要素に指定すれば、その要素だけウィンドウ幅に広げることができます。
次の要素ではimg要素を括るfigure要素に対して適用しています。img要素にはmax-width: 100%と指定して、画像のオリジナルサイズに達するまで、親要素の横幅いっぱいに広がるようにしています。
ただし、img要素にwidth/height属性の指定があると、高さが固定されてしまい横幅しか伸縮しないので、height: autoも併せて指定して、width/height属性の指定があっても縦横比を維持して伸縮するようにしています。

<section>
	<figure class="img breaking-out">
		<img src="img/event_header_bg.png" alt="" width="2000" height="400">
	</figure>
</section>
.breaking-out > img {
	max-width: 100%;
	height: auto;
}

要素で括らずに直接img要素に適用する場合には、max-width: 100%の指定があると親要素より大きくならないので、max-width: 100vwに変更してます。

<section>
	<img src="img/event_header_bg.png" alt="" width="2000" height="400" class="breaking-out">
</section>
img.breaking-out {
	max-width: 100vw;
	height: auto;
}

スクロールバー問題

記事によると、この方法だと、スクロールバーが見える状態の場合に、水平スクロールバーが表示されるようになっちゃうとのこと。vw単位は、スクロールバーの領域も含めた横幅なので、垂直スクロールバーが表示されてるとその分誤差が出ちゃうんですね。
なので、html要素body要素に対して、X方向のみスクロールバーが出ないよう指定しておくと良いです。

html, body {
	overflow-x: hidden;
}

大抵のブラウザではhtml要素かbody要素どちらか片方に指定すれば問題ないのですが、Safariでは、どちらか片方だとまだ横にスクロールできちゃうので、Safariのために両方に指定しておくとよいです。

よりよい解決

けれども、body要素にoverflow: hiddenを指定していると、body要素直下の要素をposition: stickyしたい時に困っちゃいそうで心配ですね…。
できればoverflowは使いたくないんだという場合には、左右に広げるためのネガティブマージンを、スクロールバーの幅分少なくすれば良いです。
そのために、CSS変数(カスタムプロパティ)と、JavaScriptを使います!

CSS変数

CSS変数とは、「名前を付けて保存しておける」というCSSの便利な機能。
例えば、下記のように書けば、article要素の中で使える、--variable-nameというカスタムプロパティが設定されて、プロパティの値としてvar(--variable-name)と書けば40pxが適用される、という具合に使うことができます。

article {
	--variable-name: 40px;
}
p {
	margin: var(--variable-name);
}

ここでは、要素を:root擬似クラスで指定することでHTML全体で使えるようにして、--scroll-bar-widthという名前のカスタムプロパティに、15pxという値を設定します。

:root {
	--scroll-bar-width: 15px;
}

CSS変数について、詳しくはこちらの記事を参照のこと。
CSS カスタムプロパティ (変数) の使用 - CSS: カスケーディングスタイルシート | MDN

JavaScript

それから、以下のJavaScriptで、スクロールバーが表示されているかどうかの判別と、本当のスクロールバーの幅を取得します。

const doc = document.documentElement; // <- つまり <html> のこと
if (window.innerWidth !== doc.clientWidth) {
	doc.classList.add('has-scrollbar');
	doc.style.setProperty('--scroll-bar-width', `${window.innerWidth - doc.clientWidth}px`);
}

innerWidthスクロールバーを含むビューポートの横幅を取得して、clientWidthスクロールバーを除くビューポートの横幅を取得します。なので、もしそのふたつの幅に違いがあれば、スクロールバーがあるってことになるってわけ;)
スクロールバーがあったら、html要素has-scrollbarというclass名を付けて、上で用意したCSS変数--scroll-bar-widthの値を、そのふたつの差で上書きします。

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

スクロールバーがある時、ウィンドウ幅は100vwからスクロールバーの幅を引いた値になります。左右のネガティブマージンの計算も、50% - 50vwだと、スクロールバーの幅分引き過ぎになるので、それぞれにスクロールバーの幅の半分を足せば良いってこと。つまり下記のようになります。

.has-scrollbar .breaking-out {
	margin-right: calc(50% - 50vw + var(--scroll-bar-width) / 2);
	margin-left: calc(50% - 50vw + var(--scroll-bar-width) / 2);
}

内側に戻すためのパディングには、逆に、スクロールバーの幅の半分を引けば良いですね。
それから、直接img要素に適用した時のmax-widthに指定してた100vwからも、スクロールバーの幅を引いときます。;D

.has-scrollbar .section-container {
	padding-right: calc(50vw - 50% - var(--scroll-bar-width) / 2);
	padding-left: calc(50vw - 50% - var(--scroll-bar-width) / 2);
}
.has-scrollbar img.breaking-out {
	max-width: calc(100vw - var(--scroll-bar-width));
}

さいごに

calc()関数には他にも、等分割で横並びの時の余白とか、アイコンを要素の上下中央に絶対配置する時とか、いろんな場面でいろんな面倒が端折ハショれて大変重宝しました。
2017年も引き続きお世話になると思います:)

以上、_watercolorの、「今年お世話になったCSS Advent Calendar 2016」でした!
2022年以降も引き続き、CSS変数と組み合わせてどんどん便利;D

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