お知らせ
CSSの:has()で「中身でスタイルを出し分ける」
2025.10.31お知らせ

リンクの中身に合わせて見た目を変えたい?
たとえば「画像が入っているリンクは装飾ナシ、テキストだけのリンクには矢印を付けたい」。
そんな“中身で出し分け”を CSSだけで実現できるのが :has() です。この記事では、基礎から実例までを コピペOKのコードでサクッと解説します。
2025年の主要ブラウザで実用OK。
目次
:has()ってなに?- 最初の一歩:テキストだけのリンクにだけ矢印を付ける
- よくある実例(ナビ・画像リンク・フォーム・カード)
- とりあえず覚える注意点(アクセシビリティ&パフォーマンス)
- 古い環境のフォールバック
- コピペ用チートシート
- まとめ
1. :has()ってなに?
「子孫の状態を見て、親を選べる」 新しい擬似クラスです。
HTMLをいじらなくても、内側の有無や状態で外側の見た目を切り替えられます。
親セレクタ:has( 子孫セレクタ )
a:has(img)… 画像を含むリンクa:not(:has(img))… 画像を含まないリンクa:has(> img)… 直下の子がimgのリンク(>= 子限定)
ポイント:条件分岐がCSSだけで書ける。ちょっと感動します。
2. 最初の一歩:テキストだけのリンクにだけ矢印を付ける
「画像リンクはそのまま、テキストだけのリンクにだけ印を出す」— 最初のつまずきどころを一発解決。
/* 画像やSVGを含まない a にだけ ::before を表示 */
a:not(:has(img, svg))::before {
content: "→";
margin-right: .4em;
color: currentColor;
}
<a>テキスト</a>→ 「→ テキスト」<a><img …></a>→ 何も付かない
ひとこと:
not(:has(...))は「無い時」の条件。読みながら意味が追えるのが良いところ。
3. よくある実例(ナビ・画像リンク・フォーム・カード)
「実務でどこに効くの?」を短いコードで。
3-1. ナビ:子メニューを持つ項目だけ▼マーク
メニュー項目にサブメニューがある時だけ印を出す。HTMLはそのまま。
.nav > li:has(> ul)::after {
content: "▾";
margin-left: .4em;
font-size: .9em;
}
3-2. 画像リンクだけ見た目を変える
画像を含むリンクだけ薄い枠線で区別したい時。
a:has(img) {
outline: 2px solid color-mix(in srgb, currentColor 30%, transparent);
outline-offset: 2px;
}
3-3. フォーム:フォーカスが入ったフィールドを強調
中の入力にフォーカスがある時、ラッパーを光らせる。
.field:has(:focus-visible) {
box-shadow: 0 0 0 3px color-mix(in srgb, #2563eb 30%, transparent);
}
3-4. ラベル:チェックされている時だけハイライト
<label> の中の input:checked に反応。ラジオ/チェックで有効。
label:has(input:checked) {
background: color-mix(in srgb, #2563eb 12%, transparent);
border-color: #2563eb;
}
3-5. カード:バッジを持つカードだけ余白追加
「バッジがあると窮屈」な時にサッと補正。
.card:has(.badge) { padding-top: 2rem; }
使い所イメージ:“特定の子を持つ時だけ装飾”。UIの微調整に最強。
4. とりあえず覚える注意点(アクセシビリティ&パフォーマンス)
4-1. ::before に意味のあるテキストは入れない
装飾に使うのはOK。でも意味の本体はHTMLへ。読み上げで伝わらないことがあります。
4-2. セレクタの範囲は狭く
ページ全体に重い選択をかけるとコスト増。.article a:not(:has(img)) のようにスコープを絞ると安心。
4-3. 特異性が強くなりがち
:has() の中身の特異性を引き継ぎます。強すぎたら :where() で中身の特異性を0化。
.nav:has(:where(> ul)) { /* :where 内は特異性0 */ }
5. 古い環境のフォールバック
「全部のブラウザで同じに」を強く求めないなら、そのまま運用でOK。
どうしても必要ならこの2つ。
5-1. 構造で分ける(テキストを包む)
<a href="#"><span class="label">テキスト</span></a>
<a href="#"><img src="..." alt=""></a>
a > .label::before { content: "→"; margin-right: .4em; }
5-2. JSでクラス付け(数行)
document.querySelectorAll('a').forEach(a=>{
if (a.querySelector('img,svg')) a.classList.add('is-img-link');
});
/* CSS */
a:not(.is-img-link)::before { content:"→"; margin-right:.4em; }
6. コピペ用チートシート
/* テキストだけのリンクに前置きアイコン */
a:not(:has(img, svg))::before { content:"→"; margin-right:.4em; }
/* 画像リンクだけ違う見た目に */
a:has(img) { outline: 2px solid color-mix(in srgb, currentColor 40%, transparent); }
/* 子メニューを持つ li だけ矢印 */
.nav > li:has(> ul)::after { content:"▾"; }
/* checked な input を含む label を強調 */
label:has(input:checked) { background: color-mix(in srgb, #2563eb 12%, transparent); }
/* 内側にフォーカスがあるフィールドを強調 */
.field:has(:focus-visible) { box-shadow: 0 0 0 3px color-mix(in srgb, #2563eb 30%, transparent); }
7. まとめ
:has()は「中身で親を選ぶ」。CSSだけで条件分岐できるのが最大の魅力。a:has(img)=画像あり、a:not(:has(img))=画像なし。ナビやフォームにも応用自在。- 実務では 装飾用途に限定、セレクタ範囲を絞って安全運用。
- 困ったら フォールバック(構造 or JSクラス)でカバー。
「HTMLはそのまま、見た目だけサッと分けたい」— そんな時はまず
:has()を思い出してみてください。コピペから始めて、少しずつレシピを増やしていきましょう。