モードの変更

中身のない空の div 要素や空の span 要素は HTML 仕様として妥当なのか?

HTML

この記事では JavaScript によって非同期で挿入されるコンテンツのためのプレースホルダーとしての空 div の話はしていません。以下の通り、レイアウトや装飾目的の空 div と空 span の是非について考察しています。(2021 年 9 月 6 日 19:05 ごろ追加)

レイアウトや装飾目的で、中身のない div 要素や span 要素、いわゆる「空 div」「空 span」を作ることはままある。しかしそれは仕様として妥当なのだろうか?

目次
  1. レイアウト目的の空 div の例
  2. 装飾目的の空 span の例
  3. HTML にレイアウト目的や装飾目的のための要素はない
  4. div と span の仕様から探る
    1. フローコンテンツ
    2. フレージングコンテンツ
    3. コンテンツモデルにおける text
      1. "nothing" コンテンツモデル
      2. ol, ul, menu 要素
    4. パルパブルコンテンツ
  5. カスタムエレメント
    1. トランスペアレント
  6. ここまでのあらすじ
  7. 僕の結論

レイアウト目的の空 div の例

下記は使う場所に応じて幅や高さを任意に設定することができる例だ。.Spacer が空 div になっている。

<div class="Hero">...</div>

<div class="Spacer" style="--h: 5rem"></div>

<div class="Carousel">...</div>

<div class="Spacer" style="--h: 3rem"></div>

<div class="Column">...</div>

<div class="Spacer" style="--h: 3rem"></div>

<div class="Gallery">...</div>
.Spacer {
  --w: 0;
  --h: 0;
  width: var(--w);
  height: var(--h);
}

こういうものをスペーサーコンポーネントとして利用している人も少なくないだろう。マージンの方向や相殺に悩まされることがなくなるので、使う場所で大きさを指定する点が気にならなければ便利ではある。

装飾目的の空 span の例

下記は、ハンバーガーアイコンの三本線を空 span で作っている例だ。

<button class="MenuToggler" aria-label="メニューを開く" aria-haspopup="true">
  <span class="MenuToggler__line -l1"></span>
  <span class="MenuToggler__line -l2"></span>
  <span class="MenuToggler__line -l3"></span>
</button>
.MenuToggler {
  /* button 要素のリセットCSSは省略 */
  position: relative;
  width: 50px;
  height: 50px;
  border: 1px solid currentColor;
}

.MenuToggler__line {
  position: absolute;
  top: 50%;
  left: 6px;
  width: 36px;
  height: 3px;
  background-color: currentColor;
}

.MenuToggler__line.-l1 {
  top: 15px;
}
.MenuToggler__line.-l2 {
  top: 23px;
}
.MenuToggler__line.-l3 {
  top: 31px;
}

図形が細かく HTML になっていると、 1 つ 1 つの要素に個別にアニメーションをつけられるし、擬似要素と違って JavaScript からアクセス可能になるのでマイクロインタラクションの制御に便利だ。

HTML にレイアウト目的や装飾目的のための要素はない

いきなりだが、妥当ではないと解釈できる論拠の 1 つが WHATWG HTML Standard - 3.2.1 Semantics の一節にある。

HTML Standard - 3.2.1 Semantics

Authors must not use elements, attributes, or attribute values for purposes other than their appropriate intended semantic purpose, as doing so prevents software from correctly processing the page.

“作成者は、適切に意図されたセマンティック以外の目的で、要素、属性、属性値を使用してはならない。使用すると、ソフトウェアがページを正しく処理できなくなる。”

この通り、HTML では定義以外のことをマークアップしてはならない。一見すると見出しには見出し要素を使い、表には table 要素を使うべしというシンプルな話にも見える。だが裏を返せば、意図されていないセマンティックで要素を使ってはいいけないとも読める。

すなわちレイアウト目的や装飾目的というセマンティクスが WHATWG HTML Standard に存在しない以上、div だろうが span だろうが、その目的で使って中身を空にしていい要素は何もないということになる。

div と span の仕様から探る

div 要素と span 要素の仕様を洗い直してみよう。

div 要素はフローコンテンツとパルパブルコンテンツのカテゴリーに属す。

HTML Standard - 4.4.15 The div element

span 要素はフローコンテンツとフレージングコンテンツとパルパブルコンテンツのカテゴリーに属す。

HTML Standard - 4.5.26 The span element

フローコンテンツとかパルパブルコンテンツというのは HTML5 で新設された「コンテンツモデル」という概念で、簡単に言えば「その要素がなんの要素やどんなテキストを内包できるかを定義したもの」である。似た特性は1つのグループにまとめられており、フローコンテンツやパルパブルコンテンツもそのうちの1つである。他のグループは次のリンク先で確認できる。

HTML Standard - 3.2.5.2 Kinds of content

どのグループにも含まれない固有のコンテンツモデルが指定されている要素もある。その場合グループ名は使われず、モデルの条件が文章で解説されている。

カテゴリーとは、その要素がどのコンテンツモデルに属しているかを定義したものである。複数のカテゴリーに属する要素もあるし、どのカテゴリーにも属さない要素もある。

属するカテゴリーとしてのルールと、内包できるコンテンツモデルのルール、そしてその要素ごとの特性によって、その要素の仕様を説明できるようになる。例えば title 要素では「head 要素内にしか書けない」「ドキュメント内に 1 つだけ」「内容物が空白文字のみは認められない」と言った具合だ。

属するものの説明と内包できるものの説明の両方にコンテンツモデルというキーワードが使われているのがややこしい。

それでは、div 要素と span 要素のカテゴリーになっているコンテンツモデルを確認していこう。

フローコンテンツ

フローコンテンツの仕様を原文と意訳を併記してみた。引用ブロックが原文、ダブルクォーテーションが意訳である。

HTML Standard - 3.2.5.2.2 Flow content

Most elements that are used in the body of documents and applications are categorized as flow content.

“ドキュメントの本文で使われるほとんどの要素はフローコンテンツに属している。”

a, abbr, address, area (if it is a descendant of a map element), article, aside, audio, b, bdi, bdo, blockquote, br, button, canvas, cite, code, data, datalist, del, details, dfn, dialog, div, dl, em, embed, fieldset, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, i, iframe, img, input, ins, kbd, label, link (if it is allowed in the body), main (if it is a hierarchically correct main element), map, mark, MathML math, menu, meta (if the itemprop attribute is present), meter, nav, noscript, object, ol, output, p, picture, pre, progress, q, ruby, s, samp, script, section, select, slot, small, span, strong, sub, sup, SVG svg, table, template, textarea, time, u, ul, var, video, wbr, autonomous custom elements, text

ここは訳はいらないだろう。着目すべきは最後の「text」部分。これは後述することにして、先にフレージングコンテンツを見ていく。

フレージングコンテンツ

HTML Standard - 3.2.5.2.5 Phrasing content

Phrasing content is the text of the document, as well as elements that mark up that text at the intra-paragraph level. Runs of phrasing content form paragraphs.

"フレーズコンテンツはドキュメントのテキストで、段落内のレベルでそのテキストをマークアップする要素でもある。一連のフレージングコンテンツが段落を形成する。"

a, abbr, area (if it is a descendant of a map element), audio, b, bdi, bdo, br, button, canvas, cite, code, data, datalist, del, dfn, em, embed, i, iframe, img, input, ins, kbd, label, link (if it is allowed in the body), map, mark, MathML math, meta (if the itemprop attribute is present), meter, noscript, object, output, picture, progress, q, ruby, s, samp, script, select, slot, small, span, strong, sub, sup, SVG svg, template, textarea, time, u, var, video, wbr, autonomous custom elements, text

いろいろ書いてあるがここでも最後に「text」がある。

Note: Most elements that are categorized as phrasing content can only contain elements that are themselves categorized as phrasing content, not any flow content.

"フレージングコンテンツに属する要素のほとんどは、フレージングコンテンツに属する要素だけを内包でき、フローコンテンツは内包できない。"

ここまで見ると、フローコンテンツは自身を入れ子にできるしフレージングコンテンツも自身を入れ子にできるが、フレージングコンテンツの中にフローコンテンツは入れらないとわかる。

<!-- フローコンテンツの中にフローコンテンツの例 -->
<div>
  <div>HTML仕様に即している</div>
</div>

<!-- フローコンテンツの中にフレージングコンテンツの例 -->
<div>
  <span>HTML仕様に即している</span>
</div>

<!-- フレージングコンテンツの中にフレージングコンテンツの例 -->
<span>
  <span>HTML仕様に即している</span>
</span>
<!-- フレージングコンテンツの中にフローコンテンツは記述できない -->
<span>
  <div>HTML仕様に違反している</div>
</span>

<!-- p 要素はフローコンテンツだが、コンテンツモデルがフレージングコンテンツなのでフローコンテンツを内包できない -->
<p>
  <div>HTML仕様に違反している</div>
</p>

p 要素の中に div 要素はネットサーフィンをしているとたまに見かける。これは明確に仕様に反した記述なので気をつけたい。

コンテンツモデルにおける text

text についてもちゃんと定義がある。

HTML Standard - 3.2.5.2.5 Phrasing Content - Text

Text, in the context of content models, means either nothing, or Text nodes. Text is sometimes used as a content model on its own, but is also phrasing content, and can be inter-element whitespace (if the Text nodes are empty or contain just ASCII whitespace).

“コンテンツモデル文脈におけるテキストは、何もないか、またはテキストノードを意味する。テキストはそれ自体でコンテンツモデルとして使われることがあるが、さらにフレージングコンテンツでもあり、要素間の空白になることもできる(テキストノードが空であるか、ASCII 空白文字だけの場合)”

Text nodes and attribute values must consist of scalar values, excluding noncharacters, and controls other than ASCII whitespace. This specification includes extra constraints on the exact value of Text nodes and attribute values depending on their precise context.

ここは難しいな……。

“テキストノードと属性値は、noncharactor と ASCII 空白文字以外の制御文字を除くスカラー値で構成されている必要がある。この仕様には、的確な文脈に応じたテキストノードと属性値の正確な値に対する追加の制約が含まれている。”

noncharactor と ASCII 空白文字以外の制御文字は、テキストノードと属性値にしてはいけないと読める。“的確な文脈に応じた〜追加の制約が含まれている” とはテキストノードや属性値に記述できる文字は要素や属性によって別のルールが追加されることもあるよという話だろうか? 簡単なところだと hrefに指定できる文字列には確かに「noncharactor と ASCII 空白文字以外の制御文字」の他にも、リンクとして有効な文字列であるという制約がある。そういう話だよね? 解釈が違っていたら教えてください……。

とりあえず、ASCII 空白文字ならテキストノードとして扱えるということがわかる。ASCII 空白文字の定義は、

Infra Standard - 4.5. Code Poinsts - ASCII Whitespace

ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE.

“ASCII 空白文字とは、タブ記号、LF 改行コード、フォームフィード、キャリッジリターン、半角スペースである。”

とのことなので、HTML ソース内の改行やインデント、テキストの途中に意図的に挿入する半角スペースが該当するだろう。

一応まとめ直しておくと、コンテンツモデルにおける text とは下記のいずれかに該当するものである

……おや? なんだ、要素が空っぽでもやっぱりいいんじゃん、となりかけるが、一旦おいておこう。

"nothing" コンテンツモデル

ちなみに div 要素と span 要素とは関係ないが、要素がコンテンツを何も内包できない場合は仕様では "nothing" と書かれる。これは「"nothing" コンテンツモデル」という名前で定義されていて、せっかくなので解説しておく。nothing を囲むクオーテーションの書式に違和感があるが、仕様書に準じた。

HTML Standard - 3.2.5.1 The "nothing" content model

When an element's content model is nothing, the element must contain no Text nodes (other than inter-element whitespace) and no element nodes.

“コンテンツモデルが "nothing" である要素は、要素間の空白文字以外、テキストノードも要素も含んではならない。”

Note: Most HTML elements whose content model is "nothing" are also, for convenience, void elements (elements that have no end tag in the HTML syntax). However, these are entirely separate concepts.

“注: 便宜上、コンテンツモデルが "nothing" である HTML 要素のほとんどは、HTML 構文で終了タグを持たない空要素である。ただしこれは完全に別の概念である。”

"nothing" コンテンツモデルと空要素は別概念とのことだが、では空要素ではないがコンテンツモデルが"nothing" である要素とはなんだろう?

答えは template 要素だ。template 要素の中身は DOM としての template 要素の子要素ではない。すなわち template 要素は DOM として子要素を持つことはないため、"nothing" コンテンツモデルとなっている。

HTML Standard - 4.12.3 The template element

ol, ul, menu 要素

子要素を空にすることを仕様としてはっきり認められている要素がある。それが ol 要素と ul 要素と menu 要素だ。

HMTL Standard - 4.4.5 The ol element
HMTL Standard - 4.4.6 The ul element
HMTL Standard - 4.4.7 The menu element

これらのコンテンツモデルは

Content model: Zero or more li and script-supporting elements.

とある通り、0 個以上の li 要素かスクリプトサポーティング要素(script 要素か template 要素)を含められる。つまり何も書かなくても仕様違反にならない。ついでに、1 つ以上の li 要素を含む場合は ol, ul, menu 要素はパルパブルコンテンツのカテゴリーになるが、そうでない場合はただのフローコンテンツのカテゴリーになる。

だからといって、レイアウトや装飾目的で空の ol, ul, menu 要素を使ってはいけないことは、まともに HTML を書いたことがある人間ならば容易に想像がつくだろう。

そして正直なところ、この 3 つ要素がなぜ子要素を持たなくてもよいのか、全くわからない。歴史的経緯があるのだろうか? 知っている人がいたら教えて欲しいです。


脱線した。

パルパブルコンテンツ

次はパルパブルコンテンツについて見てみよう。

HTML Standard - 3.2.5.2.8 Palpable content

As a general rule, elements whose content model allows any flow content or phrasing content should have at least one node in its contents that is palpable content and that does not have the hidden attribute specified.

“原則として、パルパブルコンテンツのカテゴリーに属する要素のコンテンツモデルが、フローコンテンツかフレージングコンテンを許容する場合は、その中身には触知可能なコンテンツが最低でも 1 つ含まれなくてはならないし、そのコンテンツに hidden 属性があってはならない。”

Note: Palpable content makes an element non-empty by providing either some descendant non-empty text, or else something users can hear (audio elements) or view (video or img or canvas elements) or otherwise interact with (for example, interactive form controls).

“パルパブルコンテンツは、読む・聞く・見る・入力するのいずれかができる要素を提供するので空にならない。”

This requirement is not a hard requirement, however, as there are many cases where an element can be empty legitimately, for example when it is used as a placeholder which will later be filled in by a script, or when the element is part of a template and would on most pages be filled in but on some pages is not relevant.

“ただし、例えばあとからスクリプトで挿入するためのプレースホルダーや、テンプレートとしてほとんどのページで使われるが一部のページでは関連性がないなど、要素が正当に空になる可能性がある場合が多いので、これは厳しい要求ではない。”

Conformance checkers are encouraged to provide a mechanism for authors to find elements that fail to fulfill this requirement, as an authoring aid.

“チェックツールは要素が合法的に空であるのかそうでないのかを検出する機能を提供することを推奨する。”

The following elements are palpable content: a, abbr, address, article, aside, audio (if the controls attribute is present), b, bdi, bdo, blockquote, button, canvas, cite, code, data, details, dfn, div, dl (if the element's children include at least one name-value group), em, embed, fieldset, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, i, iframe, img, input (if the type attribute is not in the Hidden state), ins, kbd, label, main, map, mark, MathML math, menu (if the element's children include at least one li element), meter, nav, object, ol (if the element's children include at least one li element), output, p, pre, progress, q, ruby, s, samp, section, select, small, span, strong, sub, sup, SVG svg, table, textarea, time, u, ul (if the element's children include at least one li element), var, video, autonomous custom elements, text that is not inter-element whitespace> a, abbr, address, article, aside, audio (if the controls attribute is present), b, bdi, bdo, blockquote, button, canvas, cite, code, data, details, dfn, div, dl (if the element's children include at least one name-value group), em, embed, fieldset, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, i, iframe, img, input (if the type attribute is not in the Hidden state), ins, kbd, label, main, map, mark, MathML math, menu (if the element's children include at least one li element), meter, nav, object, ol (if the element's children include at least one li element), output, p, pre, progress, q, ruby, s, samp, section, select, small, span, strong, sub, sup, SVG svg, table, textarea, time, u, ul (if the element's children include at least one li element), var, video, autonomous custom elements, text that is not inter-element whitespace

“次の要素が触知可能なコンテンツである: a, abbr, address, article, aside, audio(controls 属性があれば), b, bdi, bdo, blockquote, button, canvas, cite, code, data, details, dfn, div, dl(子要素に少なくとも 1 つの dt と dd のグループがあれば), em, embed, fieldset, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, i, iframe, img, input(type 属性が hidden ではなければ), ins, kbd, label, main, map, mark, MathML math, menu(少なくとも 1 つの li 要素があれば), meter, nav, object, ol(少なくとも 1 つの li 要素があれば), output, p, pre, progress, q, ruby, s, samp, section, select, small, span, strong, sub, sup, SVG svg, table, textarea, time, u, ul(少なくとも 1 つの li 要素があれば), var, video, 自律的なカスタムエレメント、要素間のスペースではないテキスト”

……前半 2 つのパラグラフは特に問題なく理解できる。パルパブルコンテンツのカテゴリーに属すその要素のコンテンツモデルが、フローコンテンツかフレージングコンテンツである場合は、中には意味のある何かが書かれている必要があるよ、その何かは hidden 属性が付いてたらだめだよ、その何かは読むか聞くか見るか入力するのどれかができるものだよ、そういう要素が書かれているからパルパブルコンテンツの中身が空になることはないんだよ、という説明だろう。テキスト・音声・画像・動画・canvas・インタラクティブ要素をまとめて触知可能と言っているのはちょっとオシャレだなと思う。

後半の段落はちょっと引っかかる。

This requirement is not a hard requirement, ~ と書かれているが、パルパブルコンテンツは空にならないという原則に対する例外として、when it is used as a placeholder which will later be filled in by a script だから空でもいい、はわかる。昨今の JS フレームワークと連携するための空 div などまさにそれだろうし、それが「正当に空になる場合」とすることについてはなんら異論はない。

しかし when the element is part of a template and would on most pages be filled in but on some pages is not relevant だから要素内が空でもいい、はちょっとよくわからない。一部のページで関係がないなら、その時は要素ごと出力しないようにすべきだろう。それがテンプレートというものでは?ウェブサイトのテンプレートならばそうであって欲しいと僕は感じる。私見です。 何かの互換性のためにこういう記述になっているのだろうか。

for example の他のパターンをもっと解説して欲しいところだ。その直前で there are many cases と言っているし、挙げられている 2 パターン以外にも「正当に空になる」パターンは認められているだろうとは思う。しかし、レイアウトや装飾目的で空 div や空 span を作っていいと、はっきり書かれていないゆえに、モヤモヤは晴れない。

そして原文の最後の段落は……言うは易し行うは難しすぎるだろう。lint にかけられた HTML に中身が空のパルパブルコンテンツがあったとして、それがあとからスクリプトで挿入するためのプレースホルダーであることや、テンプレートの出力結果でそうなっていることを、どうやってツールに理解させられるだろうか? 複数画面をまたいだコンテンツ仕様を機械が知っていろ、と言っているに等しい。そんなツールがあるならぜひ使いたい。

カスタムエレメント

div や span ではなくカスタムエレメントを使うという手も考えられるかもしれない。

HTML Standard - 4.13 Custom elements

class VoidFill extends HTMLElement {}
customElements.define("void-fill", VoidFill);

カスタムエレメントは HTMLElement クラスを extends した VoidFill クラスを定義して customElements.difine() で引き当てるだけだ。この時、.define()extends オプションを渡さないことで自律的なカスタムエレメントとして定義される。また、カスタムエレメントは未来の新要素との衝突を防ぐために名前にハイフンを含めなくてはならない。

HTML Standard - 4.13.3 Core consepts - valid custom element name

カスタムプロパティのタグ名は CSS ではタイプセレクタとして機能する。

void-fill {
  --w: 0;
  --h: 0;
  display: block;
  width: var(--w);
  height: var(--h);
}

HTML 側では普通に要素として記述して利用できる。

<void-fill style="--h: 3rem"></void-fill>

とまぁできるにはできるが、カスタムエレメントのコンテンツモデルはトランスペアレントである。

トランスペアレント

トランスペアレントなモデルは、自身に含められるコンテンツモデルが親要素に依存したモデルである。

HTML Standard - 3.2.5.3 Transparent content models

Some elements are described as transparent; they have "transparent" in the description of their content model. The content model of a transparent element is derived from the content model of its parent element: the elements required in the part of the content model that is "transparent" are the same elements as required in the part of the content model of the parent of the transparent element in which the transparent element finds itself.

“(前略)トランスペアレントな要素のコンテンツモデルは、その親要素のコンテンツモデルに由来します。(後略)”

ということで、カスタムエレメントを使っていようとも、親要素が既存の HTML 要素のいずれかに帰結してしまうので、レイアウトや装飾目的で内包物を空にできないと考えられる。

よしんば明確に中身を空にしていい要素があったとしても、レイアウトや装飾目的のカスタムエレメントを必ずその要素の中に入れなければならないので、使い所によっては冗長になる。ただでさえ JavaScript で定義してからでないと使えないのに、それはもう面倒すぎるだろう。

ここまでのあらすじ

できるできないにフォーカスすると、下記の感じだろうか?

div span カスタムエレメント
カテゴリー フローコンテンツ
パルパブルコンテンツ
フローコンテンツ
フレージングコンテンツ
パルパブルコンテンツ
フローコンテンツ
フレージングコンテンツ
パルパブルコンテンツ
コンテンツモデル フローコンテンツ
(dl 要素直下の div の場合は割愛)
フレージングコンテンツ トランスペアレント
フローコンテンツとしての特性 textを内包できる
(テキストノードがなくてもいい。ASCII 空白文字の場合は要素間の空白になれる)
フレージングコンテンツとしての特性 - textを内包できる
(テキストノードがなくてもいい。ASCII 空白文字の場合は要素間の空白になれる)
パルパブルコンテンツとしての特性 中身は触知可能なコンテンツでかつ要素間の空白ではないテキストを内包しなくてはいけないが、空であることに正当性がある場合は厳しく要求はしない

ふむ。ここで div 要素の仕様から下記の Note を引用したい。

HTML Standard - 4.4.5 The div element

Note: Authors are strongly encouraged to view the div *element as an element of last resort, for when no other element is suitable.

"注: 他の要素が適切ではない場合に備えて、作成者は div 要素を最後の手段とすることを強くお勧めします。"

なるほど。ちなみに、span 要素の方にはそのような注釈はない。

最終的な争点としては、

以上がコンパクトな説明となるだろう。

僕の結論

ここからは完全に僕の解釈として書く。

中身のない空の div 要素や空の span 要素は HTML 仕様として妥当か否か? 僕の結論としては「否」である。

レイアウト目的ならば marginpadding プロパティを使うのが「正当」だろう。装飾目的ならば alt 属性値を空にした img 要素や SVG が相応しい。飾りであることが触知可能になったところで別に誰も困らないし、ウェブサイトに何かセマンティクス上の不具合を生じるとも思えない。どうしても困るならば疑似要素で装飾する手もある。

marginpadding も使えないが、要素の間に空の div を挿入することはできる、というケースはきっとあるだろう。しかし、それでも marginpadding を使って管理できるように変更するのが「正当」だと僕は考える。

つまり、管理が楽とか作るのが楽とかいうのは「正当な理由」にならないと考える次第だ。現実性とか現場としてはとかいう話は正当性を覆さない。そもそもそういう場にならないように作るのが「正当性」であるし、空の div や空の span を作ればできるようなことはしてはいけないのだと思う。その場凌ぎで行うならば、そのままにせずいつか必ず修正しなくてはならないし、修正できないものに対しては行ってはならない。

僕はこれまで HTML 仕様を真面目に考えもせず、空の div や空の span をたくさん作ってきてしまった。もう修正できないものを忸怩たる思いで見つめながら、今後は空 div や空 span を生み出さず生きていこうと思う。

……まぁ本音を言えば、大きさを自由に設定できる void 要素的なものが生まれてくれたらいいなと思う。だって便利じゃん。


ぜひあなたの意見もお聞かせください。メール、ツイッター、Facebook、instagram、はてブ、いずれでもよいです。エアリプでも鬼のエゴサで拾います。