ボックスモデルを超えてゆけ!display: contents;の使い方について実例を交えながら解説
はじめに
CSS のボックスモデルは、レイアウトを構成する基本的な概念です。
しかし複雑なレイアウトを表現するのに不要なラッパー要素が増えてしまいがちで、マークアップが冗長になることがあります。
display: contents;
はこの問題を解決するための CSS プロパティの一つで、余計なボックスを作らずにマークアップをシンプルに保つことができます。
本記事では、display: contents;
の基本的な動作、実際のユースケース、考慮すべき注意点について述べていきたいと思います。
そもそもボックスモデルとは?
HTML 要素は、CSS のボックスモデルに基づいて描画されます。各要素は次のような四つの領域を持ちます。
- コンテンツ領域:テキストや画像が配置される領域
- パディング(padding):コンテンツとボーダーの間の余白
- ボーダー(border):要素の枠線
- マージン(margin):要素同士の間隔を確保するための外側の余白
display
プロパティは、このボックスモデルの振る舞いを決定します。
display
の値
代表的な -
block
:ブロックレベル要素(div
,p
など) -
inline
:インライン要素(span
,a
など) -
inline-block
:インラインのままボックスを持つ要素 -
flex
/grid
:レイアウトコンテナとして機能する要素
では、display: contents;
はどのように動作するのでしょうか?
display: contents;
の仕組みと特徴
display: contents;
の挙動
display: contents;
を指定すると、その要素自体のボックス(レイアウト上の枠組み)が消え、子要素だけが親要素の直下にあるように振る舞います。
例えば、次のような HTML があるとします。
<div class="wrapper">
<div class="content">
<p>テキスト1</p>
<p>テキスト2</p>
</div>
</div>
通常、.content
はブロック要素として扱われます。
しかし、以下の CSS を適用すると
.content {
display: contents;
}
.content
のボックスは描画されず、p
要素は .wrapper
の直下にあるかのように扱われます。
実例
例 1 ステッパー
会員登録画面や EC サイトのカート画面でよく見る以下のようなステッパーを作ってみます。
<ol>
<li aria-current="step">1</li>
<li>2</li>
<li>3</li>
</ol>
マークアップとしてはこれで良さそうです。
次に CSS で装飾をしていきます。
現時点でバーを表現する要素はないのでdiv
で作ってみると、、、
<ol>
<li aria-current="step">1</li>
<div class="bar" aria-hidden></div>
<li>2</li>
<div class="bar" aria-hidden></div>
<li>3</li>
</ol>
HTML がごちゃごちゃしてきました。
装飾のためにol
の子要素にdiv
を配置するのは、美しく感じなかったので擬似要素で作ってみます。
また、デザインデータを確認すると各ステップとバー間には16px
の余白が設けられていました。
<ol>
<li aria-current="step">1</li>
<li>2</li>
<li>3</li>
</ol>
ol {
display: flex;
gap: 16px;
}
li:not(:last-child)::after {
margin-right: 16px;
/* 以下にバー用の装飾を記述する */
}
これでも良さそうですが、ステップとバーが均等に並んでいるのに、余白の定義が複数箇所にあるのは違和感があります。
そこでdisplay: contents;
を利用して改善してみます。
ol {
display: flex;
gap: 16px;
}
+ li {
+ display: contents;
+ }
li:not(:last-child)::after {
- margin-right: 16px;
/* 以下にバー用の装飾を記述する */
}
li
の子要素(ステップと擬似要素で作ったバー)がol
要素の子要素として振る舞うようになりました。
そのためol
要素に指定していたgap
プロパティがli
の子要素にも効くようになり、不要なmargin-right
を削減することができました。
例 2 Next.js App Router での利用
次にもう少し複雑なレイアウトを作ってみます。
Next.js の App Router を使っているとします。
以下のような全ページ共通のヘッダー、サイドバーがあるような管理画面を組んでみます。
// app/layout.tsx
import "./globals.css";
export default function AppLayout({ children }: PropsWithChildren) {
return (
<html>
<body>
<header />
<aside />
<main>{children}</main>
</body>
</html>
);
}
/* app/globals.css */
body {
display: grid;
grid-template-areas:
"header header"
"aside main";
}
header {
grid-area: header;
}
main {
grid-area: main;
}
aside {
grid-area: aside;
}
<header />
, <aside />
は全ページ共通で存在するのでapp/layout.tsx
に配置しました。
// app/page.tsx
import styles from "./page.module.css";
export default function Page() {
return (
<>
<Hero className={styles.hero} />
<Content className={styles.content} />
</>
);
}
// app/blog/page.tsx
export default function BlogPage() {
return (
<>
<BlogList />
</>
);
}
<main />
配下の DOM はページ固有の UI を持つため、
app/page.tsx
に<Hero />
, <Content />
、
app/blog/page.tsx
に<BlogList />
を記述しました。
一見これで良さそうに思えますが、
768px 未満では/
の<Hero />
が<aside />
の上に配置されるようなレイアウトに変化します。
これはディレクトリ構造上、ネストしていた<Hero />
がpage.tsx
を飛び出して、layout.tsx
で定義した UI に影響を及ぼすような変化です。
これをどう実現するのか様々な方法はありますが、display: contents
とorder
プロパティで解決してみます。
CSS ファイルに以下を追加します。
/* app/globals.css */
@media (max-width: 768px) {
body {
display: flex;
flex-direction: column;
}
main {
display: contents;
}
header {
order: 0;
}
aside {
order: 2;
}
}
/* app/page.module.css */
@media (max-width: 768px) {
.hero {
order: 1;
}
.content {
order: 3;
}
}
main
にdisplay: contents;
を指定し、その子要素をbody
の子要素であるかのように振る舞わせます。
そしてbody
直下の要素を縦に並べ、order
プロパティで好きな順番に並び替えることで、
共通 UI に影響を及ぼすようなファイルをまたぐレイアウトの変更を実現してみました。
アクセシビリティへの影響
display: contents;
は スクリーンリーダーで正しく解釈されない場合がある ため、特にナビゲーション要素やフォーム要素に適用する際は注意が必要です。
ブラウザーの互換性
IE を除く主要なブラウザではサポートされています
まとめ
-
display: contents;
を指定した要素の子要素は視覚上の枠組みを抜け出し、親の直下に配置されるように振る舞う。 - CSS Grid や Flexbox と合わせて使うと有用な場面が多い。
- アクセシビリティの影響には注意が必要。
CSS の進化により、より柔軟なレイアウトが可能になることが期待されます。特に display: contents;
は、適切に使うことで HTML の構造をシンプルに保ちつつ、スタイルの自由度を高める強力なプロパティです。
今後もブラウザのアップデート情報をチェックしながら、適切な場面で活用していきましょう!
参考
Discussion