HTML を書いていると、要素に名前を付けたい場面が必ず出てきます。そのときに使うのが id 属性と class 属性です。どちらも「この要素はこれだ」と示すための属性ですが、付けられる個数のルールも、CSS での効き方も、想定されている用途も違います。この記事では、両者の基本的な違いから、CSS セレクターでの書き方と詳細度、id でなければできないこと、値に使える文字のルール、そしてクラス名の付け方の考え方までを、実際に動くコードとあわせて解説します。
目次
id と class のいちばん大きな違いは「いくつ付けられるか」
id と class はどちらもすべての HTML 要素に付けられるグローバル属性です。違いを一言でまとめると、id はページ内でただ1つの要素を指す名札、class はいくつでも貼れる分類ラベルです。
id の値は、同じ文書の中で重複してはいけません。1つの要素に id を2つ書くこともできません(属性なので1回しか書けません)。一方 class は、同じ値を何個の要素に付けても構いませんし、1つの要素に複数のクラスを半角スペース区切りで並べられます。
<!-- id はページ内で1つだけ。この main-header は他の要素に使えない -->
<header id="main-header">...</header>
<!-- class は同じ値を何度でも使える -->
<article class="card">...</article>
<article class="card">...</article>
<!-- 1つの要素に複数のクラスを半角スペースで区切って並べられる -->
<article class="card card--featured is-open">...</article>
最後の例の class="card card--featured is-open" は、「card であり、featured であり、いま開いている状態」という3つのラベルが同時に貼られている状態です。カンマではなく半角スペースで区切る点に注意してください。両者の性質を並べると次のようになります。
| 項目 | id | class |
|---|---|---|
| 同じ値を使える要素の数 | 文書内で1つだけ | いくつでも |
| 1要素に指定できる数 | 1つ | 複数(半角スペース区切り) |
| CSS セレクター | #name | .name |
| 詳細度 | 1-0-0(強い) | 0-1-0 |
| 主な用途 | 要素の識別・参照(リンク先、label、JavaScript、ARIA) | 見た目の指定・状態の表現 |
| JavaScript での取得 | getElementById() | getElementsByClassName()、querySelectorAll() |
ここから、それぞれの使い方を順に見ていきます。
複数のクラスを重ねて共通スタイルと個別スタイルを分ける
1つの要素に複数のクラスを付けられる性質は、CSS を書くうえで非常に役に立ちます。共通の見た目を1つのクラスにまとめ、違うところだけを別のクラスに切り出すという書き方ができるからです。
次のバッジは、形・余白・文字サイズを .badge にまとめ、色の違いだけを .badge--new と .badge--sale に持たせています。CSS タブの値を書き換えると、変化するのが色だけであることが確認できます。
<div class="badge-list">
<span class="badge">通常</span>
<span class="badge badge--new">新着</span>
<span class="badge badge--sale">セール</span>
</div>
.badge-list {
display: flex;
gap: 12px;
}
/* 共通の見た目は .badge にまとめて書く */
.badge {
padding: 6px 16px;
border: 1px solid #ccc;
border-radius: 999px;
background: #f7f7f7;
color: #333;
font-size: 14px;
}
/* 違うところだけを別のクラスに切り出す */
.badge--new {
border-color: #007bff;
background: #e7f1ff;
color: #0056b3;
}
.badge--sale {
border-color: #d64545;
background: #fdeaea;
color: #b02a2a;
}
もしバッジの種類ごとに .badge-new と .badge-sale という独立したクラスを作り、それぞれに余白も枠線も書いていたら、角丸を変えたくなったときに全部のクラスを直すことになります。共通部分を1か所に集めておけば修正も1か所で済みます。
.badge--new と .badge--sale は詳細度が同じ(どちらもクラス1つ)なので、.badge との勝敗はCSS に書いた順番で決まります。共通スタイルを先に、個別スタイルを後に書く、という順序を守ってください。逆に書くと個別の色が共通の色で上書きされてしまいます。
CSS セレクターでの書き方と、id のほうが強い理由
CSS で参照するときは、id には #(シャープ)、class には .(ピリオド)を先頭に付けます。属性名そのもの(id= や class=)は書きません。
/* id="main-header" の要素 */
#main-header {
background: #fff;
}
/* class に card を含む要素すべて */
.card {
border: 1px solid #ddd;
}
/* card と card--featured の両方を持つ要素だけ(間にスペースを入れない) */
.card.card--featured {
border-color: #007bff;
}
/* p 要素で、かつ note クラスを持つもの */
p.note {
font-size: 14px;
}
.card.card--featured のようにクラスを続けて書くと「両方を持つ要素」という意味になります。.card .card--featured のように間にスペースを入れると「.card の子孫にある .card--featured」という別の意味になるので、ここは間違えやすいところです。
id セレクターは class セレクターより優先される
同じ要素に別々のルールが当たったとき、どちらが勝つかを決めるのが詳細度(specificity)です。詳細度は「id セレクターの数 – class などの数 – 要素名の数」という3つの数値の組で表され、左の桁から順に比較されます。id セレクターが1つでもあれば、class をいくつ並べても勝てません。
次のデモでは、2つの段落に同じ .text-blue を付けていますが、上の段落だけ id="notice" も持っています。CSS では #notice を先に、.text-blue を後に書いていますが、上の段落は青くなりません。
<p id="notice" class="text-blue">id と class の両方が付いた段落</p>
<p class="text-blue">class だけが付いた段落</p>
p {
margin: 0 0 8px;
padding: 8px 12px;
border: 1px solid #eee;
}
/* id セレクタ(詳細度 1-0-0) */
#notice {
color: #d64545;
font-weight: bold;
}
/* class セレクタ(詳細度 0-1-0)
あとから書いても、id セレクタの色には勝てない */
.text-blue {
color: #1a73e8;
}
CSS では「あとに書いたほうが勝つ」と説明されることが多いのですが、それは詳細度が同じときだけのルールです。詳細度が違えば、書いた順番に関係なく強いほうが勝ちます。代表的なセレクターの詳細度を並べておきます。
| セレクター | 詳細度 | 意味 |
|---|---|---|
p | 0-0-1 | 要素名 |
.text-blue | 0-1-0 | クラス |
[type="text"] | 0-1-0 | 属性セレクター(クラスと同じ重み) |
:hover | 0-1-0 | 擬似クラス(クラスと同じ重み) |
.a.b.c.d | 0-4-0 | クラス4つでも #id には勝てない |
#notice | 1-0-0 | id |
style="..." | — | インラインスタイル。セレクターより強い |
スタイル指定に id を多用すると上書きしづらくなる
この強さが、スタイル指定には class を使い、id はなるべく使わないとよく言われる理由です。#main-header に色を書いてしまうと、あとから「このページのヘッダーだけ色を変えたい」と思っても、クラスを追加するだけでは上書きできません。#main-header.is-dark のように id を含めたセレクターを書き足すか、!important に頼ることになります。
そして !important を1か所で使うと、それを上書きするためにまた !important が必要になり、詳細度の管理がどんどん難しくなります。最初から class でスタイルを書いておけば、あとからクラスを1つ足すだけで見た目を変えられます。id は次に説明する「識別」の用途に取っておく、と考えるとすっきりします。
id でなければできない4つの仕事
スタイル指定に使わないのなら id は不要かというと、そんなことはありません。ページ内の特定の要素を1つ指し示すという役割は、class では代用できないからです。代表的な用途を4つ紹介します。
ページ内リンクの飛び先にする
<a> の href に # で始まる値を書くと、同じページ内の同名の id を持つ要素までスクロールします。目次から各見出しへ飛ばす仕組みは、これだけで実現できます。
<nav>
<ul>
<li><a href="#section1">導入</a></li>
<li><a href="#section2">使い方</a></li>
</ul>
</nav>
<h2 id="section1">導入</h2>
<p>...</p>
<h2 id="section2">使い方</h2>
<p>...</p>
この id は URL の一部(フラグメント)としても機能します。https://example.com/page#section2 という URL を共有すれば、開いた瞬間にその位置が表示されます。固定ヘッダーで見出しが隠れてしまう場合は、飛び先の要素に scroll-margin-top を指定すると余白を確保できます。
label と入力欄を結び付ける
<label> の for 属性に入力欄の id を書くと、ラベルと入力欄が関連付けられます。ラベルの文字をクリックしただけで入力欄にフォーカスが移り、チェックボックスならチェックが切り替わります。スクリーンリーダーも「メールアドレス、編集テキスト」のようにラベルを読み上げてくれるようになります。
<!-- for の値と id の値を一致させる -->
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email">
<label for="agree">利用規約に同意する</label>
<input type="checkbox" id="agree" name="agree">
ここで name 属性と id 属性を混同しないでください。name はサーバーに送信されるときのキーで、ラジオボタンのように意図的に同じ値を複数の要素に付けることもあります。for が見ているのはあくまで id です。
JavaScript から要素を取得する
JavaScript で特定の要素を1つだけ取り出したいときは document.getElementById() を使います。引数には # を付けない、値そのものを渡します。
// # は付けない。見つからないときは null が返る
const header = document.getElementById('main-header');
// querySelector なら CSS セレクターの記法で書く(# が必要)
const sameHeader = document.querySelector('#main-header');
// クラスで取るときは複数返る前提で書く
const cards = document.querySelectorAll('.card');
cards.forEach((card) => {
card.classList.add('is-loaded');
});
getElementById() は必ず1つの要素か null を返します。一方 querySelectorAll('.card') は該当する要素が0個でも空のリストを返すので、forEach でそのまま回せます。この違いが、そのままふたつの属性の性格を表しています。
アクセシビリティ関連の属性から参照する
WAI-ARIA の属性のうち、aria-labelledby(名前として使うテキストの参照)や aria-describedby(説明文の参照)、aria-controls(操作対象の指定)は、いずれも参照先を id で指定します。ここも class では代用できません。
<!-- 中の見出しをダイアログの名前として使う -->
<div role="dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">記事を削除しますか?</h2>
</div>
<!-- 入力欄に補足説明を結び付ける -->
<input type="password" id="password" aria-describedby="password-hint">
<p id="password-hint">8文字以上で入力してください。</p>
これらの属性は、参照先の id が存在しないとエラーにならずに黙って無視されます。綴りを間違えても画面上は何も変わらないため、気付きにくいのが厄介なところです。
値に使える文字と、避けたほうがよい書き方
HTML の仕様で id の値に課されている制限は、意外にも2つだけです。空文字にできないことと、空白文字(半角スペース、タブ、改行など)を含められないことです。それ以外は日本語でも記号でも使えます。class のほうは半角スペースが区切り文字になるため、1つのクラス名の中に空白を含められない、というのが同じ理由からの制限になります。
そしてもう1つ重要なのが、id と class の値は大文字と小文字を区別するという点です。id="mainHeader" と #mainheader は別物として扱われ、CSS も JavaScript も一致しません。スタイルが当たらない原因として実際によく遭遇します。
| 書き方 | 可否 | 説明 |
|---|---|---|
id="" | 不可 | 空文字は認められていない |
id="main header" | 不可 | 空白文字は含められない |
id="main-header" | 推奨 | 英小文字とハイフン。もっとも無難 |
id="mainHeader" | 可 | 大文字小文字は区別される点に注意 |
id="2col" | 可(非推奨) | HTML では有効だが CSS で #2col と書けない |
class="btn btn--large" | 可 | 半角スペース区切りで複数指定 |
class="btn,btn--large" | 誤り | カンマ区切りは1つの長いクラス名になる |
表の中の id="2col" について補足します。HTML5 では数字で始まる値も有効ですが、CSS のセレクターとしては書けません。CSS の識別子は数字で始められないという別のルールがあるためです。どうしても指定したい場合はエスケープが必要になり、次のような読みにくい書き方になります。
/* これは書けない(構文エラーになる) */
/* #2col { color: red; } */
/* Unicode エスケープが必要。\32 は「2」の文字コード + 末尾の空白 */
#\32 col {
color: red;
}
/* 属性セレクターで逃げる手もある */
[id="2col"] {
color: red;
}
ピリオドやコロン、スラッシュのような記号も同じくエスケープが必要になります。こうした手間を避けるため、値は英字で始め、英小文字・数字・ハイフン・アンダースコアの範囲に収めるのが実務上の安全策です。日本語の id も仕様上は有効ですが、URL のフラグメントに使うとパーセントエンコードされて読みにくくなるので避けたほうが無難です。
クラス名は「見た目」ではなく「役割」で付ける
クラス名の付け方に絶対の正解はありませんが、長く使える名前と、すぐ使えなくなる名前ははっきり分かれます。分かれ目は、見た目をそのまま名前にしていないかという点です。
<!-- 見た目そのままの名前 -->
<p class="red-text">在庫がありません</p>
<!-- 何のための文字かを名前にする -->
<p class="stock-warning">在庫がありません</p>
デザイン変更で警告の色をオレンジにすることになったとき、上の書き方だと .red-text { color: orange; } という矛盾したコードが生まれます。かといって HTML 側のクラス名も直すとなると、そのクラスを使っている全ファイルを探すことになります。.stock-warning という名前なら、CSS の色を変えるだけで済みます。
もう1つの悩みどころが名前の衝突です。.title や .item のようなありふれた名前を各所で使っていると、別の場所のスタイルが思わぬところに影響します。これを避けるために広く使われているのが BEM という命名ルールで、「Block(部品のかたまり)」「Element(部品の中の要素)」「Modifier(見た目や状態の違い)」を __ と -- でつなぎます。
<!-- card が Block、card__title などが Element、card--featured が Modifier -->
<article class="card card--featured">
<h2 class="card__title">記事のタイトル</h2>
<p class="card__body">本文の抜粋</p>
<a class="card__link" href="/article/1">続きを読む</a>
</article>
名前の先頭に必ず Block 名が付くので、.card__title と .profile__title のように用途ごとに分かれ、衝突が起きません。クラス名を見ただけでどの部品に属するか分かるのも利点です。BEM を厳密に守る必要はありませんが、「部品名を先頭に付ける」という発想だけでも取り入れる価値があります。
状態を表すクラスは、見た目のクラスと分けて is-open、is-active、has-error のような接頭辞を付ける流儀もよく使われます。JavaScript で付け外しするのはこの手のクラスに限る、と決めておくと、どのクラスが動的に変化するのかが一目で分かります。
同じ id を複数の要素に書いてしまうと何が起きるか
id の重複は HTML の仕様違反ですが、ブラウザーはエラーを出しません。ページはそのまま表示され、何も起きていないように見えます。実際には、機能ごとに違う壊れ方をします。
<!-- 仕様違反だが、ブラウザーは黙って表示する -->
<div id="item">1つ目</div>
<div id="item">2つ目</div>
<div id="item">3つ目</div>
CSS は全部に当たるので、問題に気付けない
CSS の #item は、上の3つの <div> すべてにスタイルを当てます。CSS セレクターは「条件に合う要素をすべて選ぶ」仕組みなので、id が一意かどうかを気にしないからです。見た目だけを確認していると重複に気付けず、あとで JavaScript を書いたときに初めて問題が表面化します。
JavaScript は最初の1つしか返さない
document.getElementById('item') が返すのは、文書順で最初に見つかった要素だけです。document.querySelector('#item') も同じく最初の1つを返します。3つの要素にイベントを登録したつもりが1つ目にしか付かず、「2つ目以降のボタンだけ反応しない」という症状になります。
// 「1つ目」だけが取得される
console.log(document.getElementById('item').textContent); // 1つ目
// querySelectorAll なら3つ取れるが、そもそも class を使うべき
console.log(document.querySelectorAll('#item').length); // 3
ページ内リンクと label も最初の要素だけを見る
<a href="#item"> でスクロールする先も、<label for="item"> が結び付く相手も、やはり最初の要素です。同じ id の入力欄が複数あるフォームでは、どのラベルをクリックしても先頭の入力欄にフォーカスが移ってしまいます。aria-labelledby のような ARIA の参照も同様に、意図しない要素のテキストが読み上げられます。
繰り返し表示する部分では id を組み立てる
重複がいちばん起きやすいのは、テンプレートやコンポーネントを繰り返し描画する場面です。1件分の HTML に id="email" と書いたまま一覧で10件表示すれば、その瞬間に10個の重複が生まれます。繰り返す部分で id が必要なときは、レコードの ID などを混ぜて一意になるようにしてください。
<!-- 1件目 -->
<label for="comment-12">コメント</label>
<textarea id="comment-12"></textarea>
<!-- 2件目 -->
<label for="comment-13">コメント</label>
<textarea id="comment-13"></textarea>
<label> に限っては、入力欄を <label> の中に入れてしまえば for と id の組を作らずに済みます。一意な値を作る手間を省きたいときは、この書き方も選択肢になります。
<!-- 入れ子にすれば id なしで関連付けられる -->
<label>
コメント
<textarea></textarea>
</label>
なお、重複しているかどうかを手早く調べたいときは、ブラウザーのコンソールで次のように確認できます。
// ページ内で重複している id を一覧表示する
const ids = [...document.querySelectorAll('[id]')].map((el) => el.id);
console.log(ids.filter((id, i) => ids.indexOf(id) !== i));
クラスを付けたのにスタイルが当たらないとき
「クラスを書いたのに色が変わらない」というのは誰もが一度は経験する詰まりどころです。開発者ツールで対象の要素を選び、Styles パネルにそのルールが表示されているかを見ると、原因の切り分けが一気に進みます。
ルールが一覧に出てこない場合
Styles パネルにそのルール自体が現れないなら、セレクターが要素に一致していません。ピリオドの付け忘れ(.card と書くべきところを card と書いている)、大文字小文字の食い違い、綴りの間違いが定番です。class="cards" と .card のような単数・複数のずれも起こりがちです。
クラスをカンマで区切ってしまうケースもあります。class="btn,btn--large" と書くと、ブラウザーは btn,btn--large という1つの長いクラス名として扱うので、.btn にも .btn--large にも一致しません。要素の classList をコンソールで確認すれば、実際にいくつのクラスとして認識されているかが分かります。
セレクターの構造そのものが間違っていることもあります。.card .card--featured(スペースあり)は子孫を探しにいくので、同じ要素に両方のクラスが付いていても一致しません。この場合は .card.card--featured とくっつけて書きます。
ルールは出ているが打ち消し線が引かれている場合
セレクターは一致しているのに宣言に打ち消し線が引かれているなら、他のルールに詳細度で負けています。打ち消し線の上のほうに勝っているルールが表示されているはずなので、そのセレクターを確認してください。id セレクターやインラインスタイル、!important が相手であることが多いはずです。
対処としては、勝っている側のセレクターを弱める(id セレクターをやめて class にする)のが本筋です。クラスを重ねて .card.card--featured のように詳細度を上げる方法もありますが、上げれば上げるほど今度は自分が上書きしづらくなるので、乱発は避けてください。
あとから追加した要素にだけ効かない場合
JavaScript で生成した要素にスタイルが当たらないときは、クラスの付け方を確認してください。element.className = 'is-active' と書くと既存のクラスをすべて置き換えてしまいます。もとから付いていた .card が消えるので、その分のスタイルが失われます。
// 既存のクラスが全部消える
card.className = 'is-active';
// 追加・削除は classList を使う
card.classList.add('is-active');
card.classList.remove('is-active');
card.classList.toggle('is-open');
まとめ
id はページ内でただ1つの要素を指す名札で、同じ値を2つ以上の要素に付けることはできません。class は何個の要素に付けてもよく、1つの要素に半角スペース区切りで複数指定できます。CSS では #id と .class で参照しますが、id セレクターは詳細度 1-0-0 とクラスより格段に強く、クラスをいくつ重ねても勝てません。そのためスタイル指定は class で行い、id はページ内リンクの飛び先、<label for> と入力欄の関連付け、getElementById() での取得、aria-labelledby などの参照といった「識別」の用途に使うと整理しておくと迷いません。値は空文字と空白文字が禁止で、大文字小文字は区別されます。数字始まりも HTML では有効ですが CSS セレクターでエスケープが必要になるため、英字で始めてハイフン区切りにするのが安全です。クラス名は色や大きさではなく役割で名付け、BEM のように部品名を先頭に付けると衝突を防げます。うまく動かないときは、id が重複していないか、開発者ツールの Styles パネルにルールが出ているかを最初に確認してください。