ページ内リンクを踏んだとき、飛んだ先の見出しが一瞬光ると「ここに来たんだな」と分かって親切です。この「リンク先の要素だけ」を狙うのが :target 擬似クラスです。URL の末尾に付く #id と一致する要素にスタイルを当てられるので、ハイライトはもちろん、JavaScript を書かずにタブやモーダルの表示を切り替えることもできます。この記事では基本の使い方から実践例、うまく効かないときの確認ポイントまでを動かせるデモつきで解説します。
目次
:target 擬似クラスとは
:target は、現在の URL のフラグメント(# より後ろの部分)と同じ id を持つ要素にマッチする擬似クラスです。フラグメントは「ページのどこを見ているか」を表す部分で、https://example.com/help/#setup という URL なら setup がそれにあたります。このとき id="setup" の要素が「ターゲット要素」になり、:target でスタイルを当てられます。
| URL | :target がマッチする要素 |
|---|---|
/help/(フラグメントなし) | なし(どの要素にも当たらない) |
/help/#setup | id="setup" の要素 |
/help/#faq | id="faq" の要素 |
id は文書内で重複しない値なので、ターゲット要素は多くても1つだけです。ページを読み込んだ瞬間の URL だけでなく、ページ内リンクをクリックしたときや、ブラウザの「戻る」「進む」でフラグメントが変わったときにも、:target の当たる要素は切り替わります。ボタンやチェックボックスの状態ではなく、あくまでURL の状態を見ているのがこの擬似クラスの特徴です。
リンク先の見出しをハイライトする
もっとも素直な使い方は、目次のリンクから飛んだ先を目立たせることです。飛び先の要素に id を付け、リンクの href に # + その id を書いたうえで、:target に背景色などを指定します。次のデモの上部にあるリンクを押してみてください。押したリンクに対応するセクションだけ背景色と左のラインが変わります。
<nav class="toc">
<a href="#html">HTML</a>
<a href="#css">CSS</a>
<a href="#js">JavaScript</a>
</nav>
<section id="html">
<h2>HTML</h2>
<p>ページの構造をつくるマークアップ言語です。</p>
</section>
<section id="css">
<h2>CSS</h2>
<p>見た目やレイアウトを指定するための言語です。</p>
</section>
<section id="js">
<h2>JavaScript</h2>
<p>ページに動きや処理を加えるプログラミング言語です。</p>
</section>
body {
margin: 16px;
font-family: sans-serif;
color: #333;
}
.toc {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.toc a {
color: #007bff;
}
section {
margin-bottom: 12px;
padding: 8px 16px;
border-left: 4px solid transparent;
border-radius: 6px;
transition: background-color 0.3s, border-color 0.3s;
}
section h2 {
margin: 0 0 4px;
font-size: 16px;
}
/* URL の #id と一致した section だけにスタイルが当たる */
section:target {
background: #e7f1ff;
border-left-color: #007bff;
}
section:target h2 {
color: #0a58ca;
}
CSS で書いているのは section:target の2行程度で、JavaScript は使っていません。ここでは transition を併用しているので、色がふわりと変わります。一時的に光らせて元に戻したい場合は animation を :target に指定する方法もありますが、その場合も「いつ再生するか」を決めているのは URL のフラグメントです。
なお、id が付いていれば要素の種類は問いません。見出しそのものに id を付けて h2:target とするか、見出しを含むセクションに付けて section:target とするかで、色の付く範囲が変わります。
JavaScript なしでタブを切り替える
:target は「今どこが選ばれているか」を URL に持たせられるので、表示の切り替えにも使えます。タブの中身をすべて display: none; で隠しておき、:target になったものだけ display: block; で表示すれば、それだけでタブ UI になります。タブ側のリンクは各パネルの id を指す普通のページ内リンクです。
<div class="tabs">
<nav class="tabs__nav">
<a class="tab" href="#panel-html">HTML</a>
<a class="tab" href="#panel-css">CSS</a>
<a class="tab" href="#panel-js">JavaScript</a>
</nav>
<section class="panel" id="panel-html">
<h2>HTML のタブ</h2>
<p>クリックしたタブの href と id が一致したパネルだけが表示されます。</p>
</section>
<section class="panel" id="panel-css">
<h2>CSS のタブ</h2>
<p>表示の切り替えは display の値だけで行っています。</p>
</section>
<section class="panel" id="panel-js">
<h2>JavaScript のタブ</h2>
<p>JavaScript は 1 行も使っていません。</p>
</section>
</div>
body {
margin: 16px;
font-family: sans-serif;
color: #333;
}
.tabs__nav {
display: flex;
gap: 4px;
}
.tab {
padding: 8px 16px;
border: 1px solid #007bff;
border-bottom: none;
border-radius: 6px 6px 0 0;
color: #007bff;
font-size: 14px;
text-decoration: none;
}
.panel {
display: none; /* パネルはいったんすべて隠す */
padding: 12px 16px;
border: 1px solid #007bff;
border-radius: 0 6px 6px 6px;
}
/* URL の #id と一致したパネルだけを表示する */
.panel:target {
display: block;
}
/* どのタブも選ばれていない初期状態では、最初のパネルを表示する */
.tabs:not(:has(:target)) .panel:first-of-type {
display: block;
}
/* 選択中のタブを塗りつぶす */
.tabs:has(#panel-html:target) .tab[href="#panel-html"],
.tabs:has(#panel-css:target) .tab[href="#panel-css"],
.tabs:has(#panel-js:target) .tab[href="#panel-js"],
.tabs:not(:has(:target)) .tab:first-of-type {
background: #007bff;
color: #fff;
}
.panel h2 {
margin: 0 0 8px;
font-size: 16px;
}
ひとつ工夫がいるのが「まだどのタブも押していない最初の状態」です。フラグメントが空のあいだは :target にマッチする要素がないため、素直に書くとパネルが1枚も表示されません。そこで .tabs:not(:has(:target))、つまり「中に :target の要素を持たないとき」という条件で最初のパネルを表示しています。選択中のタブの色も同じ考え方で、:has() を使って親側から「どのパネルが :target か」を判定しています。
モーダルやアコーディオンにも応用できる
同じ「隠しておいて :target のときだけ見せる」という考え方は、簡易的なモーダルにも使えます。開くボタンをモーダルの id へのリンクにして、閉じるボタンはフラグメントを消すリンク(href="#")にします。フラグメントが空になればどの要素も :target ではなくなるので、モーダルが閉じます。
<a class="button" href="#notice">お知らせを見る</a>
<div class="modal" id="notice">
<div class="modal__inner">
<h2>お知らせ</h2>
<p>JavaScript を使わずに開閉しています。</p>
<!-- フラグメントを空に戻して閉じる -->
<a href="#">閉じる</a>
</div>
</div>
.modal {
display: none; /* ふだんは隠しておく */
position: fixed;
inset: 0;
place-items: center;
background: rgba(0, 0, 0, 0.5);
}
/* URL が #notice のときだけ表示する */
.modal:target {
display: grid;
}
.modal__inner {
background: #fff;
border-radius: 8px;
padding: 24px;
}
アコーディオン(開閉メニュー)も理屈は同じで、見出しを本文の id へのリンクにし、本文を :target のときだけ開けば作れます。ただし :target はターゲット要素が常に1つなので、この方法では複数の項目を同時に開いておけません。開閉が目的なら、キーボード操作や支援技術への対応が最初から備わっている details / summary 要素を使うほうが素直です。:target による開閉は「URL で開いた状態を共有したい」ときの選択肢と考えるとよいでしょう。
:target-within は使えるのか
:focus-within の :target 版として :target-within という名前を目にすることがありますが、これは現在の CSS Selectors Level 4 の仕様に含まれておらず、MDN にも項目がありません。ブラウザにも実装されていないので、記事やスニペットで見かけても実際には動かないと考えてください。
「子孫に :target の要素があるとき、その親にスタイルを当てたい」という目的なら、前述のタブのデモのように :has(:target) で書けます。:has() は主要ブラウザで使えるようになっているため、こちらが実質的な代替です。
スタイルが当たらないときに確認すること
:target は仕組みが単純なぶん、効かないときの原因もだいたい決まっています。順に確認してみてください。
href と id が一致していない
いちばん多いのがこれです。href 側は # を付けて href="#setup"、要素側は # を付けずに id="setup" と書きます。id="#setup" のように # まで含めてしまうと一致しません。またフラグメントと id の照合は大文字と小文字を区別するので、#Setup と id="setup" は別物です。
スタイルを当てたい要素そのものを狙えていない
:target がマッチするのは id を持つ要素だけで、その親や兄弟には当たりません。たとえば id を見出しに付けているのに、色を付けたいのが見出しを囲むカード全体であれば、h2:target では足りません。id を囲む側の要素に移すか、.card:has(:target) のように親から辿るか、あるいは #panel:target ~ .note のような兄弟結合子を使う必要があります。
JavaScript で URL を書き換えている
history.pushState() や history.replaceState() で URL を変更しても、ターゲット要素は更新されません。SPA のルーターなどがこれらのメソッドで履歴を操作している場合、URL の見た目が #setup に変わっても :target は反応しないことがあります。一方、history.back() などの履歴移動ではターゲット要素は更新されます。なお、シャドウ DOM(Web Components)の内側の要素にも :target は当たりません。
別ページのアンカーに向けたリンクを踏んでいる
:target が見るのは、あくまで今表示している文書の URL とその中の id です。href="/help/#setup" のように別ページのアンカーへリンクした場合、スタイルが当たるのは移動先のページにある id="setup" の要素であって、リンク元のページでは何も起きません。移動先のページで CSS が読み込まれていなければ、当然ハイライトも表示されません。
履歴とスクロールの扱いに注意する
:target で表示を切り替えるということは、状態を URL に持たせるということです。この性質には良い面と困る面の両方があります。
良い面は、開いた状態をそのまま共有・ブックマークできることです。「3番目のタブを開いた URL」を相手に送れば、相手も同じ表示から読み始められます。また、フラグメントが変わるたびに履歴が1つ増えるため、ブラウザの「戻る」で直前の状態に戻れます。モーダルを開いた直後に「戻る」を押せばモーダルが閉じる、という自然な挙動が自動的に手に入ります。
困る面もその裏返しです。タブを何度も切り替えると履歴がどんどん積み上がり、前のページへ戻るのに「戻る」を何度も押すことになります。さらに、フラグメントへの移動はページ内リンクそのものなので、ブラウザはターゲット要素が見える位置までスクロールします。ページ下部にあるタブを切り替えたときに画面が上下に飛ぶのはこのためです。固定ヘッダーに隠れる程度なら scroll-margin-top で調整できますが、大きく飛ぶのが困る場面では details 要素や JavaScript による切り替えも検討してください。
まとめ
:target は、URL のフラグメント(#id)と一致する id を持つ要素にマッチする擬似クラスです。リンク先のハイライトのような小さな演出から、タブや簡易モーダルのような表示切り替えまで、JavaScript なしで実装できます。効かないときは href と id の綴り、狙っている要素が id の付いた要素自身かどうか、URL を pushState() で書き換えていないかを確認しましょう。表示の切り替えに使うときは、履歴が増えることとスクロールが起きることを前提に設計するのがポイントです。