CSSでリストをデザインするための完全ガイド

CSSでリストをデザインするための準備

HTMLにはリストを作成する要素がいくつか用意されていますが、ここでは順序リストを対象に解説を行います。順序リストを作成するには、<ul>要素または<ol>要素を使用し、リストの中に並ぶ項目を<li>で作成します。

これらの要素に対してスタイルを適用していくわけですが、実際にマークアップを始める前にボックス・モデルの知識を予習しておきましょう。ボックスに関する知見が備わっていないと、リストのデザインでつまずくことがあります。なぜなら、リストは入れ子構造になっており、複数のボックスを操作することが前提だからです。

また、ブラウザごとにHTMLのレンダリング・エンジンが異なるため、CSSでデザインする場合は複数の環境で確認しながら作業しましょう。主要なブラウザで起こる問題が、もう一方のブラウザでは起こらないという現象が、しばしば見受けられるからです。

ここでは、リスト関連のプロパティの知識があるものとして解説を行います。

順序リストの基本構造を押さえる

順序リストの構造は、リスト全体のボックスとリスト項目を作る子要素の入れ子です。しかし、そこには他の要素にはない後付の要素が発生します。それが::markerという擬似要素です。

::markerは、displayプロパティの値がlist-itemの要素に付与されるもので、既定値では<li><summary>が該当します。この存在を把握することが、順序リストの基本構造を理解するための第一歩です。特に、CSSでリストをデザインする祭に上手くいかない原因のひとつがこれです。

::markerは、ChromeやFirefoxなどのブラウザが採用している仕様であるため、WebKit系のブラウザでは動作が異なる場合があります。以下に示す内容は、一部の環境やスマートフォン向けのモバイル・ブラウザ全てに一致するわけではありません。しかし、リストの基本を押さえるという意味では、非常に大事な知識です。

マーカーを表示させたくない場合は、list-style-typeの値にnoneを指定します。この時、マーカーが表面的に見えなくなるのではなく、擬似要素の::markerが作成されなくなるという点を覚えておきましょう。

それでは、ボックスの構造を可視化して::markerがどこに配置されているのかを確認します。マーカーが配置される位置はlist-style-positionの値によって変わります。

list-style-positionの既定値はoutsideです。そのため、このプロパティ自体を省略すると、マーカーはリスト項目の外側に配置されます。これをinsideに変更すると、マーカーは<li>要素が作り出したコンテンツ・ボックスの内側に配置されます。

ここで注意しなければいけない点があります。それは、HTMLのソースコード上で::markerが差し込まれる位置です。これに関しては、list-style-positionの値がどうであれ、<li>要素の直下に生成されます。この仕様に基づくブラウザでは次のように解釈されます。私たちは、これを踏まえた上でCSSを操作していかなければならないのです。


<ul>
	<li>::marker "Item 1"</li>
	<li>::marker "Item 2"</li>
	<li>::marker "Item 3"</li>
</ul>

リストデザインの大まかな流れ

CSSでリストをデザインする祭の大まかな流れを紹介します。ここで大事なのは、自分が何のために何のプロパティを操作するのか、しっかりと把握することです。曖昧な認識のままプロパティを操作すると、リストマーカーの位置や要素同士の距離を期待通りに調整できなくなります。

以下の流れを見て、ボックスの構造やリストの所在を把握して下さい。慣れてくれば、各々のやり方で順番を変えてしまって問題ありません。これはあくまで基本の流れをつかむための参考例です。

マーカーのポジションが "outside" の場合

list-style-positionの値がoutsideの場合、マーカーはリスト項目の外側に配置されます。この時、リストを構成する要素とマーカーの余白の取り方に気をつける必要があります。

::marker<li>要素の直下に作成されますが、画面に描画される時点でリスト項目のコンテンツ・ボックスから外れて、パディング・ボックスおよびボーダー・ボックスの外側へ飛び出します。つまり、<li>要素のpaddingborderの幅を広げると、コンテンツから距離が離れるのです。

ただし、マージン・ボックスの内側に所属しているため、marginの変化には追随します。この位置関係を十分に把握しておかないと、マーカーや要素同士の位置取りが上手くいきません。

完成時の内容は以下の通りです。リストの内部構造の余白をpaddingで統一している点に注目して下さい。ここで下手にmarginを組み合わせると、複雑性が増して子要素を入れ子にした時に調整が難しくなります。


<ol id="step5">
	<li>Item 1</li>
	<li>Item 2</li>
	<li>Item 3</li>
</ol>
ol#step5 {
	margin: .5rem 0 0;
	padding: 0 1rem 0 2rem;
	border: 1px solid #999;
	list-style-type: decimal;
	list-style-position: outside;
}
ol#step5 > li {
	padding: .3rem .5rem;
	border-bottom: 1px dotted #999;
}
ol#step5 > li:last-child {
	border: none;
}

マーカーのポジションが "inside" の場合

list-style-positionの値をinsideにすると、マーカーはリスト項目の内部に配置されます。これはマーカーが<li>要素のコンテンツ・ボックスに含まれることを意味し、ボーダー・ボックスおよびパディング・ボックスの内側に所属している状態となります。

つまり、<li>要素のpaddingborderの幅を変更しても、コンテンツとの距離を変えません。これは一見メリットのように思えますが、マーカーとコンテンツの距離を調整することが難しくなるという側面もあります。

以下の例は、子要素を含まない単純なリストをデザインする場合の流れです。list-style-positionの値がoutsideの場合とは勝手が違うので、比較してみましょう。

完成時の内容は以下の通りです。borderが重複して太く見える問題を最終段階で解消しています。マーカーを表示させない場合は、list-style-typeの値をnoneにします。


<ul id="step5">
	<li>Item 1</li>
	<li>Item 2</li>
	<li>Item 3</li>
</ul>
ul#step5 {
	margin: .5rem 0 0;
	padding: 0;
	border-top: 1px solid #999;
	border-right: 1px solid #999;
	list-style-type: circle;
	list-style-position: inside;
}
ul#step5 > li {
	padding: .5rem 1rem;
	border-bottom: 1px solid #999;
	border-left: 1px solid #999;
}

::markerをセレクタにできるのか

CSSに慣れている方であれば、擬似要素として作成される::markerをセレクタとして、あらかじめ用意しておいたスタイルを適用したいと考えるかもしれません。

実は、方法論としては有効ですが、指定できるプロパティが限られています。そのため、::markerを自由にデザインできるとは思わない方が良いでしょう。

リスト項目のマーカーがずれる現象

リスト項目に子要素を配置した時にマーカーがずれることがあります。この問題は、特にリンク要素を配置してクリックできる範囲を広げようとした時に起こります。

まずは状況を確認してみましょう。次の例は、<li>要素に指定していた余白を<a>要素に移し替えて、<a>要素が余白の寸法を受け入れるようにdisplayプロパティの値をblockにした場合の挙動です。これを複数のブラウザで見比べてみると、どうなるでしょうか。

上記のリストは、list-style-positionの値がinsideのデザインです。この場合、擬似要素の::markerはリスト項目のパディング・ボックスおよびボーダー・ボックスの内側に配置されます。

リンクをブロック要素に変えたことで、クリックできる範囲は広がりました。しかし、ブロック要素と同じ行に擬似要素が存在するため、当然のことながら改行が発生します。これがリストマーカーがずれて表示される原因です。

同じ内容をWebKitベースのSafariで表示しても、問題ないように見えます。これはリストマーカーを作成するアルゴリズムが異なるからです。開発者が常に複数の環境でテストしないと見落としてしまう仕様の一種だと言えます。

この問題に対応する解決策をいくつか用意しました。それらのうち、どの方法を取るかは状況とデザインの目的によって変わります。まずは、順序リストの基本構造を思い出してから先へ進みましょう。

マーカーの位置を変えるか無くす

マーカーが子要素の邪魔をしているのであれば、マーカーをリスト項目のコンテンツ・ボックスから追い出せば、レイアウトは上手く行きます。この場合、list-style-positionの値をoutsideにする方法と、list-style-typeの値をnoneにする方法の二種類が考えられます。

しかし、今回のデザインはリスト項目を境界線で囲うものです。マーカーがボックスの外側へ出ると、根本的に余白の取り方やデザインを見直す必要があります。また、マーカーを残したい場合もあるでしょう。その時は、次の方法を試して下さい。

マーカーを自分でデザインする

マーカーを自由にデザインするためには、::markerを非表示にして、替わりに::before擬似要素を<li>要素に付加します。

::before擬似要素は::markerと同じようにブロック要素に干渉しますが、こちらは位置指定を受け入れるため、positionプロパティの値をabsoluteにします。すると、レイアウトの通常フローから::beforeが外れるため、子要素のボックスが横幅一杯に伸びることができます。

マーカーとなる記号はcontentプロパティで定義します。あとは、大きさや位置を整えれば完成です。


<ul>
	<li><a href="#">Item 1</a></li>
	<li><a href="#">Item 2</a></li>
	<li><a href="#">Item 3</a></li>
</ul>
ul {
	margin: .5rem 0 0;
	padding: 0;
	border-top: 1px solid #ccc;
	border-right: 1px solid #ccc;
	list-style: none;
}
ul > li {
	position: relative;
	border-bottom: 1px solid #ccc;
	border-left: 1px solid #ccc;
}
ul > li::before {
	content: "○";
	position: absolute;
	top: 25%;
	left: 1em;
	font-size: 0.6875rem;
}
li > a {
	display: block;
	padding: .3rem 1rem .3rem 2rem;
}
li > a:hover,
li > a:active {
	background-color: #eee;
}

順序リストのカウントを擬似要素で再現する

マーカーを擬似要素で再現した場合、番号をカウントする順序つきのリストが使えなくなるという懸念がありますが、安心して下さい。これもCSSで解決できます。

CSSカウンターという機能を使うと、ページの中に登場する要素の数を数えることができます。カウンターの数値は、counter-incrementプロパティを使って増減させます。

実は、順序つきリストを作成する<ol>の子要素として配置されるリスト項目は、暗黙のうちにlist-itemというカウンターが与えられています。通常は、自動的に機能しているため意識することはありませんが、CSSカウンターを使っているという点では同じです。

次の例を見て下さい。ブロック要素のリンク範囲を維持しつつ、リスト項目のナンバリングを実現しています。


<ol>
	<li><a href="#">Item 1</a></li>
	<li><a href="#">Item 2</a></li>
	<li><a href="#">Item 3</a></li>
</ol>
ol {
	margin: .5rem 0 0;
	padding: 0;
	border-top: 1px solid #ccc;
	border-right: 1px solid #ccc;
	list-style: none;
}
ol > li {
	position: relative;
	border-bottom: 1px solid #ccc;
	border-left: 1px solid #ccc;
	counter-increment: list_num;
}
ol > li::before {
	content: counter(list_num) ". ";
	position: absolute;
	top: 25%;
	left: 1em;
	font-size: 0.875rem;
}
li > a {
	display: block;
	padding: .3rem 1rem .3rem 2rem;
}
li a:hover,
li a:active {
	background-color: #eee;
}

特別な記述が見られるのは、<li>要素に追加したcounter-incrementプロパティと、::before擬似要素に指定したcontentプロパティの値です。

counter-incrementプロパティは、カウントの対象となるセレクタに与えます。そして、その値は任意の文字列で作成した識別子です。この名前をcounter()関数の中に入れると数を数えます。それをcontentプロパティで出力しているのです。

やっていることは、順序なしリストの時と変わりませんが、見覚えのないプロパティが増えるので、少し難しく感じます。慣れてくれば簡単なので、コピーしてからアレンジしてみましょう。

入れ子のリストをデザインする

入れ子構造のリストをデザインする場合のコツは、親子要素に共通するスタイルと片方のリストに適用するスタイルを分けて考えることです。まずは、既定値の状態を見てみましょう。特に、HTMLのソースコード上で親子関係をしっかりと確認しておきます。


<ul id="nest_list">
	<li>Item 1
		<ul>
			<li>1-1</li>
			<li>1-2</li>
			<li>1-3</li>
		</ul>
	</li>
	<li>Item 2
		<ul>
			<li>2-1</li>
			<li>2-2</li>
			<li>2-3</li>
		</ul>
	</li>
	<li>Item 3
		<ul>
			<li>3-1</li>
			<li>3-2</li>
			<li>3-3</li>
		</ul>
	</li>
</ul>

リストを入れ子にする場合、似たようなタグを何度も目にすることになります。そのため、ついid名やclass名を与えてセレクタに使用しがちですが、ここでは親子関係に注目にした方が上手くいきます。

以下の例は、全ての余白を均一にしたリストのデザインです。ここではpaddingの値を1remにしています。少し極端な配色にしているのは、ボックスと入れ子の関係を把握しやすくするためです。最も親の階層のリストが起点になるようにidをひとつだけ用意し、それ以下のスタイルはグルーピング・セレクタのみを採用しています。

#nest_list {
	margin: 1rem 0 0;
	padding: 0 1rem 1rem;
	border: 1px solid #ccc;
	background-color: #fc9;
	list-style-type: circle;
	list-style-position: inside;
}
#nest_list li {
	margin: 1rem 0 0;
	padding: 1rem;
	border: 1px solid #ccc;
	background-color: #ff9;
}
#nest_list ul {
	padding: 0;
	background-color: #cfc;
}
#nest_list ul > li {
	background-color: #9cf;
}

このように記述すると、入れ子にした要素に共通して適用するスタイルと、下の階層にのみ適用するスタイルを分けて管理できます。全ての階層にidclassを用意すると、それらを並列に扱わなければならず管理が難しくなります。

開閉式の多段リストを作成する

入れ子構造のリストを応用すれば、CSSだけで開閉式の多段リストを作成できます。次の例は、<input>要素のcheckboxをトリガーとしたやり方です。

リスト項目の見出しを<label>要素で作成し、それが押された時にチェックボックスの状態が変化するようにidで連携させます。その変化をセレクタに紐づけて、下層のリストに異なるスタイルを適用します。

この仕組みは様々な場所で応用可能なので、覚えておくと便利なテクニックです。初めて知る方は、ソースコーソを良く読んで、どのように動作しているのか確認して下さい。

入れ子構造のリストの作り方は前の章で解説しているので、分からなくなった場合は戻って予習しましょう。


<ul id="nest_list">
	<li>
		<input type="checkbox" id="menu1">
		<label for="menu1">Menu 1</label>
		<ul>
			<li><a href="#" target="_blank">1-1 Item</a></li>
			<li><a href="#" target="_blank">1-2 Item</a></li>
			<li><a href="#" target="_blank">1-3 Item</a></li>
		</ul>
	</li>
	<li>
		<input type="checkbox" id="menu2">
		<label for="menu2">Menu 2</label>
		<ul>
			<li><a href="#" target="_blank">2-1 Item</a></li>
			<li><a href="#" target="_blank">2-2 Item</a></li>
			<li><a href="#" target="_blank">2-3 Item</a></li>
		</ul>
	</li>
	<li>
		<input type="checkbox" id="menu3">
		<label for="menu3">Menu 3</label>
		<ul>
			<li><a href="#" target="_blank">3-1 Item</a></li>
			<li><a href="#" target="_blank">3-2 Item</a></li>
			<li><a href="#" target="_blank">3-3 Item</a></li>
		</ul>
	</li>
</ul>
#nest_list {
	margin: 1rem 0 0;
	padding: 0;
	border-right: 1px solid #ccc;
	border-bottom: 1px solid #ccc;
	background-color: #fff;
	list-style: none;
}
#nest_list li {
	position: relative;
	border-top: 1px solid #ccc;
	border-left: 1px solid #ccc;
}
#nest_list ul {
	display: none;
	margin-left: 1rem;
	padding: 0;
	list-style: none;
}
#nest_list a {
	display: block;
	padding: .3rem 1rem;
	color: #139;
}
#nest_list a:hover {
	background-color: rgba(0, 153, 255, 0.1);
}
#nest_list input {
	position: absolute;
	white-space: nowrap;
	width: 1px;
	height: 1px;
	overflow: hidden;
	border: 0;
	padding: 0;
	clip: rect(0 0 0 0);
	clip-path: inset(50%); 
	margin: -1px;
}
#nest_list label {
	position: relative;
	display: block;
	padding: .5rem 1rem .5rem 2rem;
	cursor: pointer;
}
#nest_list label::before {
	content: "≡";
	position: absolute;
	top: 0;
	left: .5rem;
	color: #999;
	font-size: 1.5rem;
}
#nest_list label:hover {
	background-color: rgba(0, 0, 0, 0.1);
}
#nest_list input:checked ~ label::before {
	content: "▼";
	top: 25%;
	font-size: 0.875rem;
}
#nest_list input:checked ~ ul {
	display: block;
}

開閉式の多段リストを作成する時のポイント

CSSで開閉する要素を作成する祭のポイントを解説します。まずは初期状態で閉じている下層のリストをデザインします。ここでは以下の部分が該当するセレクタです。

#nest_list ul {
	display: none;
	margin-left: 1rem;
	padding: 0;
	list-style: none;
}

displayの値がnoneになっているため、下層のリスト全体が不可視となっています。それ以外は装飾用途のプロパティです。続いて、<input>要素のcheckboxに変化があった時に表示するスタイルを定義します。

#nest_list input:checked ~ ul {
	display: block;
}

ここで注目すべきはセレクタです。HTMLのチェックボックスをチェック状態にした時、checkedというフラグが立ちます。これはマウスオーバオーに反応する:hoverなどと同じように、擬似クラスとして扱えます。これをトリガーとして、同じ階層に属する要素にマッチする一般兄弟結合子(~)を使って<ul>を特定します。

これで、チェックボックスが有効になった時にだけ現れるリストが実現できました。チェック状態を無効にすると、:checked擬似クラスの対象から外れるため、またdisplayの値がnoneの状態に戻ります。

ちなみに、<input>要素を隠すために指定しているスタイルはvisually-hiddenというスニペットで、アクセシビリティの低下を防ぐためのものです。

トランジション効果を与えてスムーズに開閉させる

リストの開閉をスムーズに見せるには、transitionプロパティを使います。トランジション効果は、ある状態とある状態の切り替わりに時間経過を与えるものです。今回は、チェックボックスのONとOFFを扱うため、animationよりも適しています。

トランジション効果を作成するポイント

トランジション効果を使うにあたって、下層リストの初期状態に適用するスタイルを変更しています。以下の部分を見てみましょう。

#nest_list ul {
	overflow: hidden;
	height: 0;
	margin-left: 1rem;
	padding: 0;
	line-height: 0;
	list-style: none;
	transition: 0.2s;
}

前回のサンプルは、displayの値で見える状態と見えない状態を切り替えていましたが、今回はheightline-heightの値を0にして、ボックスの寸法を無くします。overflowは、ボックスから溢れるテキストを隠す役目です。

transitionは、時間経過を与えたいプロパティを指名するのですが、今回は全てのプロパティを対象にしたいため、省略して時間だけを明記します。これで下準備が整いました。

続いて、下層リストが見える状態になった時のスタイルです。前回のサンプルと同様に:checked擬似クラスで、下層リストを特定します。ここで指定するプロパティは、リスト要素の寸法を0にしていたheightline-heightです。ここに有効な寸法を指定すると、要素全体が可視化される祭に、transitionで定義した時間の変化が再生されます。

#nest_list input:checked ~ ul {
	height: auto;
	line-height: 1.5;
}

横並びのリストを作成する

CSSで横並びのリストを作る場合はフレックス・ボックス、あるいはグリッド・レイアウトを使います。どちらを使うべきかは、目的と条件によって異なります。

大きく分類すると、単一行に収まるリストを作成する場合はフレックスを採用します。初めから複数行に渡るデザインであることが決まっているのであればグリッドを採用します。

フレックスで横並びのリストを作成する

フレックスを使って横並びのリストを作成する場合は、まず初めに親要素となる<ul>または<ol>displayプロパティを与え、flexを指定します。これで子要素として配置された<li>要素は、全てフレックス・アイテムとして扱われます。

あとは、コンテナの装飾とアイテムの装飾を適用していけば完成です。難しく考える必要はありません。コンテナに指定するプロパティと、アイテムに指定するプロパティを、しっかりと分類できていれば大丈夫です。

以下の例は、コンテナの幅が変わった時にアイテムを折り返さないリストと、アイテムを折り返して行を増やすリストの比較です。resizeに対応しているブラウザであれば、サンプルの幅を変えられます。横幅を狭めていった時にどうなるか、実際に動かして確認してみましょう。


<div class="container">
	<section>
		<h1>flex nowrap</h1>
		<ul class="flex_list" id="fw_nowrap">
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
		</ul>
	</section>
	<section>
		<h1>flex wrap</h1>
		<ul class="flex_list" id="fw_wrap">
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
		</ul>
	</section>
</div>

.container {
	overflow: auto;
	padding: 0 1rem 1rem;
	background-color: #eee;
	resize: both;
}
.container > section {
	margin: 1rem 0 0;
	padding: 1rem;
	background-color: #fff;
}
section > h1 {
	margin: 0;
	font-size: 1rem;
}
/* リストのスタイル */
.flex_list {
	overflow: scroll;
	display: flex;
	flex-direction: row;
	gap: .5rem;
	margin: 1rem 0 0;
	padding: .5rem;
	background-color: #333;
	list-style: none;
}
.flex_list li {
	flex: 1;
}
.flex_list a {
	display: block;
	padding: .5rem;
	border: 1px solid #ccc;
	background-color: #fff;
	text-align: center;
	color: #333;
}
.flex_list a:hover {
	background-color: #09f;
	color: #fff;
}
#fw_nowrap {
	flex-wrap: nowrap;
}
#fw_wrap {
	flex-wrap: wrap;
}

グリッドで横並びのリストを作成する

グリッドを使って横並びのリストを作成する場合は、まず初めに親要素となる<ul>または<ol>displayプロパティを与え、gridを指定します。これで子要素として配置された<li>要素は、全てグリッド・アイテムとして扱われます。

グリッド・レイアウトを作成する手順は、各プロパティのページで解説していますが、基本的にはコンテナとなる親要素でテンプレートを定義し、そこへアイテムを並べていくことになります。

以下の例では、コンテナにgrid-template-columnsを指定してアイテムを横並びにしています。各アイテムの寸法やコンテナの幅が変わった時に折り返すのかどうかの定義は、全てこのプロパティで行っています。


<div class="container">
	<section>
		<h1>Grid nowrap</h1>
		<ul class="grid_list" id="gl_nowrap">
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
		</ul>
	</section>
	<section>
		<h1>Grid wrap</h1>
		<ul class="grid_list" id="gl_wrap">
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
			<!--=== 必要な個数並べる ===-->
			<li><a href="#" target="_blank">Item</a></li>
			<li><a href="#" target="_blank">Item</a></li>
		</ul>
	</section>
</div>

.container {
	overflow: auto;
	padding: 0 1rem 1rem;
	background-color: #eee;
	resize: both;
}
.container > section {
	margin: 1rem 0 0;
	padding: 1rem;
	background-color: #fff;
}
section > h1 {
	margin: 0;
	font-size: 1rem;
}
/* リストのスタイル */
.grid_list {
	overflow: scroll;
	display: grid;
	gap: .5rem;
	margin: 1rem 0 0;
	padding: .5rem;
	background-color: #333;
	list-style: none;
}
.grid_list li {
	min-width: 5rem;
}
.grid_list a {
	display: block;
	padding: .5rem;
	border: 1px solid #ccc;
	background-color: #fff;
	text-align: center;
	color: #333;
}
.grid_list a:hover {
	background-color: #09f;
	color: #fff;
}
#gl_nowrap {
	grid-template-columns: repeat(5, 1fr);
}
#gl_wrap {
	grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr));
}

CSSリファレンス一覧