12k
All articles

base-select でセレクトドロップダウンをスタイリングする

appearance: base-selectでネイティブselectメニューをCSSで装飾。ピッカー、選択肢、アイコン、アクセシビリティ、ブラウザー対応、制約を解説します。

OpenReplay Team
OpenReplay Team
base-select でセレクトドロップダウンをスタイリングする

<select> とその ::picker(select) 疑似要素に appearance: base-select を設定すると、ボタン、ドロップダウン、矢印、チェックマーク、各オプションまで、コントロール全体を CSS でスタイリングできます。

多くのチームがカスタムドロップダウンコンポーネントを保守している理由は、ネイティブの <select> をデザインに合わせてスタイリングできなかったという一点に尽きます。キーボード操作、フォーカス管理、ARIA の設定は、その副産物として付いてきたものです。appearance: base-select は、この理由をなくします。

このガイドでは、オプトインからリッチなオプションまで、スタイリング可能な範囲をひととおり解説し、その代わりに失うものも取り上げます。従来の appearance: none を使った手法については、モダン CSS による select 要素のスタイリングで全体像を解説していますが、base-select には簡単に触れているだけです。より古いアプローチについては、JavaScript を使わずにセレクトドロップダウンをスタイリングする方法をご覧ください。

要点

  • appearance: base-select は select と ::picker(select) の両方に設定する必要があります。ピッカーだけに設定しても効果はありません。
  • ピッカーはトップレイヤーに表示されるポップオーバーで、暗黙のアンカー参照によって select ボタンにアンカーされます。そのため、z-index、ポータル、anchor-name は不要です。
  • <selectedcontent> は <select> の最初の子要素である <button> の中に配置し、閉じた状態のコントロールに選択中のオプションの複製を表示します。
  • すべてのオプションにテキストを含めてください。アイコンだけのオプションでは、スクリーンリーダーが読み上げる内容がなく、非対応ブラウザーでは空のオプションとして表示されます。
  • base-select では、モバイル OS のピッカー、ブラウザーの表示領域外へのレンダリング、最も長いオプションに合わせた自動サイズ調整が使えなくなります。

削除できるコンポーネント

カスタムセレクトコンポーネントは、たいていネイティブの <select> 要素の見た目を補うためだけに存在し、その過程で多くの振る舞いを抱え込んでいます。Chrome チームのリリース記事によれば、Chrome 135 以降では <select> 要素を CSS でスタイリングできるようになり、組み込みコントロールが持つアクセシビリティと標準の振る舞いはそのまま維持されます。WebKit も Safari 27.0 でこれに続き、本物の <select> を隅々までスタイリングし直せるようになりました。ブラウザーが対応すれば、div ベースのリストボックスを維持する主な理由はなくなり、それを支えるために再実装していた振る舞いも不要になります。

appearance: base-select にオプトインするには

オプトインするには、<select> とそのピッカーの両方に appearance: base-select を適用します。MDN のカスタマイズ可能な select ガイドによると、select は単独でオプトインできますが、ピッカーは select もオプトインして初めて機能します。WebKit のカスタマイズ可能な select に関するガイダンスでは、これらの拡張を @supports (appearance: base-select) で囲むことを推奨しています。以降のルールもすべて同じブロック内に記述し、非対応ブラウザーでは通常の select が表示されるようにしましょう。

@supports (appearance: base-select) {
  select,
  ::picker(select) {
    appearance: base-select;
  }
  /* all following rules go here */
}
パーツセレクター備考
閉じた状態のコントロールselectselect ボタン
矢印::picker-iconアクセシビリティツリーには含まれない
開いた状態:openピッカーの表示中にマッチ
ドロップダウン::picker(select)最初の子要素であるボタン以外のすべて
各選択肢optionデフォルトで display: flex
選択中の選択肢option:checked
選択マーク::checkmarkアクセシビリティツリーには含まれない
閉じた状態のコンテンツselectedcontent選択中のオプションの複製

閉じた状態のコントロールをスタイリングする

閉じた状態のコントロールは select 要素です。矢印は ::picker-icon で、:open はピッカーの表示中にマッチするため、select が開いたときに矢印を回転させることができます。

select {
  border: 2px solid #d4d4d8;
  background: #f4f4f5;
  padding: 10px;
  transition: 0.4s;
}

select:hover,
select:focus {
  background: #e4e4e7;
}

select::picker-icon {
  color: #71717a;
  transition: 0.4s rotate;
}

select:open::picker-icon {
  rotate: 180deg;
}

rotate に対する transition によって、ピッカーの開閉のたびに矢印が滑らかに回転します。

開いたリストをスタイリングする

カスタマイズ可能な select の開いたドロップダウンリストは ::picker(select) です。ブラウザーは、先頭のボタンを除く <select> 内のすべての要素をこの中にまとめます。オプションはデフォルトでフレックスコンテナーです。:checked は選択中のオプションを、::checkmark はその横のマークを対象にします。

::picker(select) {
  border: none;
  border-radius: 8px;
}

option {
  display: flex;
  gap: 16px;
  padding: 10px;
}

option:hover,
option:focus {
  background: #ede9fe;
}

option:checked {
  font-weight: bold;
}

option::checkmark {
  order: 1;
  margin-left: auto;
  content: "✔";
}

order: 1 と margin-left: auto を設定すると、チェックマークが行の先頭から末尾に移動します。

配置とアニメーション

カスタマイズ可能な select では、ドロップダウンはトップレイヤーに開くポップオーバーです。ブラウザーが CSS アンカーポジショニングによって自動的に select ボタンと関連付けるため、z-index、ポータル、anchor-name の設定は必要ありません。ボタンとピッカーの間には、Invoker Commands ガイドでボタンとポップオーバーについて説明しているのと同じ invoker/popover の関係があります。また、デフォルトのスタイルでは、ピッカーが画面からはみ出す場合に別の側へ移動します。anchor() を使って独自の位置を指定することもできます。

::picker(select) {
  top: calc(anchor(bottom) + 1px);
  left: anchor(10%);
}

ピッカーをフェードイン・フェードアウトさせるには、ポップオーバーの状態間でトランジションを設定します。

::picker(select) {
  opacity: 0;
  transition: all 0.4s allow-discrete;
}

:open::picker(select) {
  opacity: 1;
}

@starting-style {
  :open::picker(select) {
    opacity: 0;
  }
}

allow-discrete により、離散的なプロパティである display と overlay もトランジションに参加できるようになります。これがないと、フェードアウトが見える前にピッカーがトップレイヤーから外れてしまいます。@starting-style は、フェードインの開始時の不透明度を設定します。

リッチなオプションと selectedcontent

<selectedcontent> は、<select> の最初の子要素である <button> の中に配置します。これにより、閉じた状態のコントロールに選択中のオプションのコンテンツの複製が表示され、ドロップダウン内のオプションとは別にスタイリングできます。オプションには、アイコンや画像などのマークアップを含められるようになりました。

<label for="status">Status</label>
<select id="status" name="status">
  <button>
    <selectedcontent></selectedcontent>
  </button>
  <option value="draft">
    <span class="icon" aria-hidden="true">📝</span>
    <span class="option-label">Draft</span>
  </option>
  <option value="review">
    <img class="icon" src="review.svg" alt="">
    <span class="option-label">In review</span>
  </option>
  <option value="published">
    <span class="icon" aria-hidden="true">✅</span>
    <span class="option-label">Published</span>
  </option>
</select>

MDN によると、アイコンによって閉じた状態のボタンが縦に大きくなり、矢印の位置がずれることがあります。閉じた状態のときだけアイコンを非表示にできます。

selectedcontent .icon {
  display: none;
}

開いたピッカー内のオプションでは、アイコンはそのまま表示されます。select ボタンは inert であるため、中に配置したリンクやボタンを個別にフォーカスしたりクリックしたりすることはできず、全体が 1 つのボタンとして機能します。また MDN は、ボタン内に余分なコンテンツがあるとスクリーンリーダーが読み上げる select の値が変わる可能性があると指摘しています。ボタンの中身は <selectedcontent> だけにしておきましょう。

ブラウザーが代わりに処理してくれること

base-select では、カスタムドロップダウンが再実装しなければならない処理、つまりキーボード操作、フォーカス、トップレイヤーでの重なり順、値のフォーム送信、スクリーンリーダー向けのセマンティクスをブラウザーが担います。WebKit は、あくまでブラウザー自身のコントロールであるため、キーボード対応とアクセシビリティが組み込まれていると述べています。div ベースのドロップダウンでユーザーセッションをリプレイすると、キーボードやフォーカスのバグが見つかることがよくありますが、ネイティブの select はそうしたバグを設計上回避できます。手作業によるアプローチにどれだけのコードが必要かは、アクセシブルなドロップダウンセレクトを自作するこちらのガイドを参照してください。

ただし、次の 2 つのルールは引き続き守る必要があります。

  • すべてのオプションにテキストを含める。 WebKit の最も重要なルールは、各オプションに実際のテキスト、またはアクセシブルなテキスト属性が必要だというものです。視覚的に非表示にしたテキストも、スクリーンリーダーには読み上げられます。装飾目的の画像には alt="" を指定し、意味はテキストラベルに担わせましょう。
  • カスタムアイコンは装飾として扱う。 ::checkmark や ::picker-icon に content で追加したものはスクリーンリーダーに無視されるため、カスタムの矢印やチェックマークが読み上げられることはありません。

select の値を読み取る既存の JavaScript はそのまま動作します。オプションに value 属性がない場合、select の値は選択中のオプションの textContent から前後の空白を除いたものになります。そのため、オプションにリッチなマークアップを含める場合は、必ず value を設定してください。

サポート状況、フォールバック、トレードオフ

appearance: base-select は、Chrome 135 以降、Edge などの Chromium ベースのブラウザー、Safari 27.0 で利用できます。Firefox でも設定フラグの背後で実装が始まっており、appearance: base-select の値は Firefox 149 でフラグ付きで導入されましたが、デフォルトでは有効になっていません。MDN ではまだ「Limited availability」とされているため、MDN のリファレンスページにある互換性一覧を確認してください。なお、Safari は独自のデフォルト base-select スタイルを持っているため、スタイルを指定していないコントロールの見た目はブラウザー間で一致しません。

base-select に対応していないブラウザーは、<button> と <selectedcontent> のマークアップを無視します。その結果、各オプションがテキストのみになった、通常どおり動作する普通の select がレンダリングされます。このフォールバックが成り立つのは、テキストが存在する場合だけです。

Chrome の記事では、失われる振る舞いとして次の 3 つも挙げられています。

  • 開いたリストはブラウザーウィンドウ内に収まります。
  • スマートフォンやタブレットでは、OS 独自のピッカーではなくブラウザーのピッカーが表示されます。
  • select の幅が最も長いオプションに合わせて調整されなくなるため、select, ::picker(select) に自分で width を設定する必要があります。

また、base-select は HTML パーサーによる <select> 内部の解釈も変えます。MDN は、これが JavaScript フレームワークと衝突する可能性があると警告しています。フレームワークによってはこれらの機能をブロックし、また別のフレームワークではサーバーサイドレンダリングによってハイドレーションが壊れることがあります。既存のコンポーネントを削除する前に、使用しているフレームワークでこの機能をテストしてください。

まとめ

appearance: base-select を使ったネイティブの select なら、スタイリングを自由にコントロールしつつ、キーボード、フォーカス、重なり順、アクセシビリティはブラウザーに任せられます。試すには、シンプルなカスタムドロップダウンを 1 つ選び、すべてのオプションにテキストを含めたうえで @supports ブロック内で作り直し、base-select に対応していないブラウザーで確認してみましょう。そこでも普通の select として問題なく動作し、上記のトレードオフを受け入れられるなら、カスタムコンポーネントは削除できます。

よくある質問

select における appearance: none と appearance: base-select の違いは何ですか?

appearance: none は、閉じた状態のボタンのネイティブスタイルを取り除くだけです。ドロップダウンリストとそのオプションには引き続きブラウザーや OS のピッカーが使われ、ほとんどの CSS は適用されません。一方、select と ::picker(select) の両方に appearance: base-select を設定すると、ピッカー、オプション、矢印、チェックマークもスタイリングでき、オプション内にリッチな HTML を含めることもできます。非対応ブラウザーはこれを無視し、従来のコントロールを表示します。

JavaScript で base-select のサポートを検出するには?

CSS.supports('appearance', 'base-select') を呼び出します。この値を認識するブラウザーでのみ true が返ります。CSS の条件 @supports (appearance: base-select) と同じ内容をチェックするため、フォールバック用のドロップダウンコンポーネントを読み込むかどうかの判断や、カスタムスタイルを有効にするクラスの付与に利用できます。エンジンごとに対応時期が異なるため、ユーザーエージェントの判定ではなく機能検出を使用してください。

base-select は select multiple や size 属性でも使えますか?

はい、リストボックスモードで使えます。multiple 属性、または 1 より大きい size 値を指定すると、オプションはポップアップを開くボタンではなく、ページ上のボックスとして表示されます。Chrome 145 以降では、このモードでも単一選択・複数選択の両方で base-select が機能します。ただし、multiple と size=1 を組み合わせたときの複数選択ポップアップにはまだ対応していません。カスタマイズ可能なリストボックスにはピッカーがないため、::picker(select)、::picker-icon、selectedcontent は適用されません。他のエンジンでのサポート状況は MDN の互換性一覧を確認してください。

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.