レスポンシブ対応のレイアウトやUIコンポーネントの実装が簡単! min(), max(), clamp() の便利な使い方を詳しく解説
Post on:2022年10月13日
CSSの比較関数が主要ブラウザすべてにサポートされてから早2年が経ちました。これらの比較関数は、今まではJavaScriptを使用しないとできなかったこと、メディアクエリで複雑に実装していたことをシンプルなCSSで実装できます。
最近のWebサイトで使用されている、CSSの比較関数 min()
, max()
, clamp()
の便利な使い方を紹介します。
Use cases for CSS comparison functions
by Ahmad Shadeed
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
- はじめに
- 比較関数の使用例: 流動的なサイジングとポジショニング
- 比較関数の使用例: 装飾要素
- 比較関数の使用例: 流体ヒーローの高さ
- 比較関数の使用例: ローディングのバー
- 比較関数の使用例: コンテンツの区切り線
- 比較関数の使用例: スマホとデスクトップで異なる角丸
- 比較関数の使用例: 防御的なCSSの記事ヘッダ
- 比較関数の使用例: ビューポートの幅に応じて変化するスペース
- 終わりに
はじめに
CSSの比較関数は2020年4月頃からサポートされており、私はその当時に記事を書きました。
翻訳記事: CSSの比較関数が便利すぎる! min(), max(), clamp()の使い方を詳しく解説
比較関数のどれも好んで使用していますが、中でも一番好きなのはclamp()
で、使用率がトップの関数です。
この記事では、比較関数の使用例を取りあげ、それぞれについて詳しく解説しようと思います。使用例のほとんどは流動的なサイジングに関するもので、もっとも一般的な使用例だと思います。
CSSの比較関数について知らなくても、全然大丈夫です。前述の記事を読んでから、この記事に戻って使用例を見ることをお勧めします。
【訳者注】CSSの比較関数は、すべての主要ブラウザにサポートされています。
min(), max(), clamp()のサポートブラウザ
比較関数の使用例: 流動的なサイジングとポジショニング
1つ目はスマホのモックアップ画像とその上に2つの画像があるコンテナです。初期状態では、下記のようになります。
モックアップ画像とその上に配置される2つの画像
コンテナの幅が小さくなったときに、使用可能なスペースに合わせて画像のサイズを縮小したいとします。そのためには、幅や高さに%
値(width: 20%;
など)を使用することでできますが、%
値では正確に制御できません。
そのためには、最小値と最大値を定義できる流体サイズを使用したいと思います。ここでclamp()
の出番です!
1 2 3 |
.section-image { width: clamp(70px, 80px + 15%, 180px); } |
clamp()
の3つのパラメーターは、最小値、推奨値、最大値です。上記のCSSは、最小値が70px
、推奨値が80px + 15%
、最大値が180px
に定義しています。
これで画像の幅を流動的なサイジングで実装できます。
画像の幅を流動的なサイジングで実装
最小値、推奨値、最大値を設定することで、画像はコンテナの幅に応じて縮小または拡大されます。これは固定値とパーセント値(80px + 15%
)を組み合わせて使用しているのがポイントです。
実際の動作は、デモページをご覧ください。
See the Pen
Comparison Functions - Percentage sizing by Ahmad Shadeed (@shadeed)
on CodePen.
比較関数の使用例: 装飾要素
セクションに装飾的な要素を追加することはありませんか? ほとんどの場合、要素はレスポンシブである必要があり、ビューポートのサイズに応じて異なる位置に配置変更する必要があるでしょう。
たとえば、下記のように左上と右下に装飾要素がある場合で考えてみましょう。
左上と右下に装飾要素がある
装飾要素は左右に2つあります。スマホではスペースを取り過ぎてしまうので、それぞれを少しだけ表示したいと思います。
スマホではサイズを変更して表示
これを実装するためには、メディアクエリを使用してスマホでオフセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.decorative--1 { left: 0; } .decorative--2 { right: 0; } @media (max-width: 600px) { .decorative--1 { left: -8rem; } .decorative--2 { right: -8rem; } } |
上記のCSSでも機能しますが、CSSのclamp()
関数を使用すれば、メディアクエリなしで簡単に実装できます。
1 2 3 4 5 6 7 8 9 |
@media (max-width: 600px) { .decorative--1 { left: clamp(-8rem, -10.909rem + 14.55vw, 0rem); } .decorative--2 { right: clamp(-8rem, -10.909rem + 14.55vw, 0rem); } } |
上記のCSSで何をしているか解説します。
- 必要なのは、
left
のオフセットの最小値を-8rem
に、最大値を0rem
にすることです。 - あとはその間の推奨値を設定し、設定した最小値と最大値を尊重するのはCSSの
clamp()
関数に任せます。
CSSのclamp()
関数で実装
clamp()
関数の数値は、Min-Max-Value Interpolationを使用して算出しました。
実際の動作は、デモページをご覧ください。
See the Pen
Comparison Functions - Decoration by Ahmad Shadeed (@shadeed)
on CodePen.
比較関数の使用例: 流体ヒーローの高さ
前述の使用例に関連して、ヒーローセクションの高さがビューポートのサイズに応じて異なる場合があります。その結果、メディアクエリやビューポート単位を使用して実装することがあると思います。
ビューポートのサイズに応じて高さが異なるヒーローセクション
メディアクエリを使用するとこんな感じのCSSになります。
1 2 3 4 5 6 7 8 9 |
.hero { min-height: 250px; } @media (min-width: 800px) { .hero { min-height: 500px; } } |
固定値とビューポート単位を組み合わせることもできますが、大きなビューポートでは高さが大きくならないように注意する必要があり、その場合は最大高さを設定する必要があります。
1 2 3 4 5 6 7 8 9 |
.hero { min-height: calc(350px + 20vh); } @media (min-width: 2000px) { .hero { min-height: 600px; } } |
CSSのclamp()
を使用すると、1つの宣言だけで最小・推奨・最大の高さを設定できます。
1 2 3 |
.hero { min-height: clamp(250px, 50vmax, 500px); } |
スクリーンのサイズを変更すると、ビューポートの幅に応じて高さが変化することが分かります。上記の推奨値(50vmax
)は「ビューポートの最大サイズの50%」を意味します。
CSSのclamp()
で実装
比較関数の使用例: ローディングのバー
この使用例はAndy Bellのツイートにインスパイアされたものです。clamp()
の使い方がとても気に入っています!
ローディングのバー
バーのサム(つまみ)は左から右へ、またはその逆へアニメーションする仕様になっています。CSSで、サムは絶対的に左に配置できます。
1 2 3 |
.loading-thumb { left: 0%; } |
サムを右に配置するにはleft: 100%;
を使用しますが、これには問題があります。というのは、サムがローディングのコンテナからはみ出てしまうからです。
サムがローディングのコンテナからはみ出る
CSSは下記の通りです。
1 2 3 |
.loading-thumb { left: 100%; } |
このコンテキストでは100%
100%がサムの端から始まり、押し出されるため、当たり前の結果です。
こういった場合、多くの人はcalc()
でサムの幅を引き算すると思います。しかし、このCSSには柔軟性がありません。
1 2 3 4 |
.loading-thumb { /* 40px represents the thumb width. */ left: calc(100% - 40px); } |
CSSの変数と比較関数を使用して、改善する方法を解説します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
.loading-thumb { --loading: 0%; --loading-thumb-width: 40px; position: absolute; top: 4px; left: clamp( 0%, var(--loading), var(--loading) - var(--loading-thumb-width) ); width: var(--loading-thumb-width); height: 16px; } |
上記のCSSの仕組みは以下の通りです。
- 最初に、最小値として
0%
を設定します。 - 推奨値は、CSSの変数
--loading
の現在の値です。 - 最大値は、現在のローディングからサム幅を引いたものです。
CSSのclamp()
は、このコンポーネントに対して3つの異なる状態を提供します。私は個人的にこのソリューションが好きです!
CSSの仕組み
それだけでなく、同じコンセプトを別のデザインに拡張することも可能です。
ローディングのバー
現在の値には小さなハンドルがあります。値が100%
のとき、それを尊重する幅が必要です。
下図にあるように、ハンドルの円は右端で終了する必要があります。この点に気をつけないと、ハンドル幅の半分くらいが吹き飛ばされてしまいます(下図の2つ目を参照)。
上: 期待する実装、下: ハンドルがはみ出している
そんな時はCSSのclamp()
関数を使用します。
1 2 3 |
.loading-progress { width: clamp(10px, var(--loading), var(--loading) - 10px); } |
最小値は円の幅の半分に等しく、推奨値は現在のパーセント値、最大値は円の半分から現在のパーセントを減算した結果になります。
CSSの仕組み
実際の動作は、デモページをご覧ください。
See the Pen
Comparison Functions - Progress by Ahmad Shadeed (@shadeed)
on CodePen.
比較関数の使用例: コンテンツの区切り線
今年の初め、私が取り組んでいたUIの興味深いCSSソリューションについての記事を公開しました。
翻訳記事: CSSで区切り線を実装するのは、flexboxが簡単で便利! レスポンシブ対応の区切り線を実装するテクニック
2つのセクション間にある区切り線です。
コンテンツの区切り線: デスクトップでの表示
スマホではこの区切り線は、水平になります。
コンテンツの区切り線: スマホでの表示
記事での解決策は、border
とFlexboxを使用するものでした。このアイデアはborder
を持つ疑似要素があり、垂直と水平方向の両方の状態で利用可能なスペースを占めるというものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
.section { display: flex; flex-direction: column; gap: 1rem; } .section:before { content: ""; border: 1px solid #d3d3d3; align-self: stretch; } @media (min-width: 700px) { .section { align-items: center; flex-direction: row; } } |
CSSのclamp()
を使用すると、さらに改善できます。
Temani Afifは、メディアクエリをまったく必要としないソリューションを提案しています。
1 2 3 4 5 6 7 8 9 10 11 |
.section { --breakpoint: 400px; display: flex; flex-wrap: wrap; } .section:before { content: ""; border: 2px solid lightgrey; width: clamp(0px, (var(--breakpoint) - 100%) * 999, 100%); } |
上記のCSSの仕組みは以下の通りです。
0px
: 垂直方向の区切り線に使用される最小値。CSSのborder
を代わりに使用しているので0です。(var(--breakpoint) - 100%) * 999
ビューポートの幅に応じて、0px
と100%
を切り替えるトグル。
実際の動作をアニメーションGIFにしました。
区切り線が垂直・水平方向に切り替わる仕組み
比較関数の使用例: スマホとデスクトップで異なる角丸
これは1年くらい前に見つけた、Facebookのフィードで使用されている巧みなCSSのテクニックです。max()
比較関数を使用してビューポートの幅に応じてカードの角丸を0pxから8pxに切り替えます。
参考: Facebookの新しいUIデザインで見つけたCSSのテクニックのまとめ
スマホとデスクトップで角丸を切り替える
CSSは下記の通り。
1 2 3 4 5 6 |
.card { border-radius: max( 0px, min(8px, calc((100vw - 4px - 100%) * 9999)) ); } |
上記のCSSの仕組みは以下の通りです。
0px
とmin()
で計算された値を比較するmax()
関数があります。max()
関数は2つの値を比較して、大きい値を選択します。min()
関数では、8px
とcalc((100vw - 4px - 100%) * 9999)
で計算された値を比較します。この結果は、非常に大きな正または負の値が得られます。9999
は、値を強制的に0px
または8px
のどちらかにするための数値です。
上記のようにすると、カードがビューポートの幅いっぱいになっているときは角丸の半径が0px
になり、大きなスクリーンでは8px
になります。素晴らしいと思いませんか?
このテクニックの詳細については、下記をご覧ください。
翻訳記事: レスポンシブ対応にメディアクエリなしで、CSSの関数で定義!border-radiusの値を変えるテクニック
比較関数の使用例: 防御的なCSSの記事ヘッダ
Defensive CSSの記事ヘッダを実装しているときに、小さいビューポートで最小値を維持しながら、コンテンツに動的なパディングを与える方法が必要でした。
動的なパディングを与える
記事のヘッダにはラッパーがなく、ページのヘッダとコンテンツにはラッパーがあります。このラッパーの有無に関係なく、整列させる方法が必要です。
記事のヘッダにはラッパーがないけど、他と整列させたい
そのためには、CSS で次の式を使用する方法が必要です。
1 |
dynamic padding = (viewport width - wrapper width) / 2 |
CSSのmax()
関数のおかげで、最小限のパディングと必要なときに動的なパディングに切り替えることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
:root { --wrapper-width: 1100px; --wrapper-padding: 16px; --space: max( 1rem, calc( ( 100vw - calc(var(--wrapper-width) - var(--wrapper-padding) * 2) ) / 2 ) ); } .article-header { padding-left: var(--space); } |
実装のポイントは、padding
の最小値を1rem
として、それをビューポートの幅に応じて動的に変化させるというものです。
このテクニックの詳細については、下記をご覧ください。
翻訳記事: CSSのレイアウトで、ラッパーが異なるコンテンツのツラをcalc()関数のパディングで揃える
比較関数の使用例: ビューポートの幅に応じて変化するスペース
ビューポートの幅に応じてコンポーネントやグリッドの間隔を変更する場合があります。CSSの比較関数を使用すれば、ほんの数行で簡単に実装できます。
1 2 3 4 5 |
.wrapper { display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: min(2vmax, 32px); } |
実際の動作をアニメーションGIFにしました。
ビューポートの幅に応じて変化するスペース
CSSのスペーシングについて詳しく知りたい方は深堀り記事を書きましたので、ご覧ください。
終わりに
この記事があなたのお役に立てれば幸いです。
コメントや提案があれば、@shadeed9までお願いします。
sponsors