:has() 選択式カード
ラジオを隠したプラン選択カード。選ぶとそのカードだけ枠と背景が変わり、チェックが点く。:has()だけで実現しJSは不要。
プレビュー
クリック
操作クリック/タップで動きます。
仕様書
- 向く場面
- プラン・配送方法など1つを選ぶカード
- ラジオをカード全体で押させたい選択
- JS なしで選択状態を装飾したいとき
- 避ける場面
- 複数選択(radio ではなく checkbox)
- 選択肢が多く縦に長い一覧
- 構成要素
- グループ見出し(legend)
- カード
- チェック丸
- プラン名と価格
- 補足
- 持っている状態
- 通常
- フォーカス
- キーボード
Tab でラジオ群へ、↑↓/←→ で選択を移動(ネイティブ radio)。Space で選択
- 支援技術
ネイティブ radio をカード全面に opacity 0 で重ねるため role と checked はそのまま伝わり、当たり判定もカード全体。fieldset/legend で群名。選択は枠色+背景+チェック記号。:has(:focus-visible) でリング
- 動き
枠・背景・チェックの拡大を 180ms でトランジション。選んだ瞬間の軽い応答。reduced-motion では即時
目的選ばせる
デザインの要点
- radio は display: none にせず opacity 0 でカード全面に重ねる。消すとキーボードと読み上げから消え、全面に重ねれば当たり判定もカード全体になる
- 装飾は label:has(input:checked) で label 側に当てる。input + span の兄弟セレクタでは label の枠色までは届かない
- 選択の手がかりは枠色・背景・チェック記号の 3 つ。色覚差でも記号 1 つで判別できる
- フォーカスリングは :has(input:focus-visible) で label に描く。透明な input のリングは見えない
実装メモ
:has() 関係擬似クラスで「中のラジオが checked である label」を選び、選択カードだけを装飾しています。ラジオ自体は視覚的に隠しつつネイティブのまま残すので、キーボード操作・アクセシビリティは保たれ、JavaScriptは不要です。:has() は主要ブラウザで広く利用可能です。
コード(コピーして使えます)
<fieldset class="hs-group">
<legend class="hs-legend">プランを選択</legend>
<label class="hs-card">
<input type="radio" name="plan" value="free">
<span class="hs-check" aria-hidden="true">✓</span>
<span class="hs-name">Free</span>
<span class="hs-price">¥0<small>/月</small></span>
<span class="hs-note">個人利用向けの基本機能</span>
</label>
<label class="hs-card">
<input type="radio" name="plan" value="pro" checked>
<span class="hs-check" aria-hidden="true">✓</span>
<span class="hs-name">Pro</span>
<span class="hs-price">¥1,280<small>/月</small></span>
<span class="hs-note">制限解放とチーム共有</span>
</label>
<label class="hs-card">
<input type="radio" name="plan" value="team">
<span class="hs-check" aria-hidden="true">✓</span>
<span class="hs-name">Team</span>
<span class="hs-price">¥3,800<small>/月</small></span>
<span class="hs-note">管理機能と優先サポート</span>
</label>
</fieldset>