<selectmenu>
タグできる子; <select>
に代わるカスタマイズ可能なドロップダウンリスト
<select>
タグと自作ドロップダウンリスト
HTML の <select>
タグ(HTMLSelectElement
)は制限が多く、ちょっと凝ったことをしようと思うとすぐ <select>
では足りなくなります。たとえば、選択肢にテキストコンテンツ以外(アイコンなど)を含めたい、とか、フィルター用の入力ボックスをつけたい、とか、コンボボックスにしたい、とか。複数選択用の <select multiple>
は、 PC ブラウザーで見るとポップアップではなく静的なリストボックスになり、 Ctrl + クリックなど複雑な操作が必要になるため扱いづらく、使われることは稀かと思います。
そこで独自ドロップダウンリストを実装することになるのですが、ちゃんと実装しようと思うと面倒です。
- セマンティックマークアップ
- HTML タグ、 WAI-ARIA のロールやステートなど適切に指定します。
- キーボード操作
- [↑][↓][PageUp][PageDown][Home][End] で選択肢移動、 [Space] や [Enter] で決定、 [Esc] で閉じる、などに対応します。
- ポップアップの向き
- トリガーボタンが画面下部にあるなら上向きにポップアップさせます。
- ポップアップの高さ
- 画面に収まる範囲でなるべく高くします(多くの選択肢がパッと見えるように)。
- ポップアップ表示時のスクロール位置
- 選択中の選択肢が見えるよう
scrollIntoView()
的なことをします。
- 選択中の選択肢が見えるよう
- ポップアップの座標計算
- Bootstrap のドロップダウンのように、ポップアップをトリガーボタンの隣接要素にすれば座標計算は不要です。ただこれだと
overflow: visible
でない祖先要素が存在する場合にポップアップがはみ出た分だけ隠れてしまいます(Bootstrap のドロップダウンで開発者ツールを開いてbd-example
クラスを持つ要素にoverflow: hidden
をつけてみてください)。overflow: hidden
などで隠れないようにするには、ポップアップを<body>
の子要素にして、座標をgetBoundingClientRect()
、scrollX
、scrollY
あたりから計算する方法が考えられます。ですが<body>
配下だと今度は<dialog>
内にドロップダウンを配置してshowModal()
したときダイアログの背後に隠れてしまいます。たとえ<dialog>
を使わないようにしても、<body>
に CSS のtransform
やzoom
が設定されている場合は座標を逆変換したり、トリガーボタンの祖先にposition: fixed
の要素が存在する場合はスクロール追従のためposition: fixed
にしたり、非常に煩雑です。
- Bootstrap のドロップダウンのように、ポップアップをトリガーボタンの隣接要素にすれば座標計算は不要です。ただこれだと
<selectmenu>
タグ
<selectmenu>
(HTMLSelectMenuElement
)はドロップダウンリストのために新しく提案されている HTML タグです。上記のようなちょっと面倒な実装をまるっと請け負ってくれる上、容易にカスタマイズできるようデザインされています。
現在、 Chrome Canary や Edge Canary の about:flags
ページで Experimental Web Platform features フラグを有効にすると使えるようになります。
<selectmenu>
参考リンク:
- 紹介記事
- Say Hello to selectmenu, a Fully Style-able select Element – CSS-Tricks
https://css-tricks.com/the-selectmenu-element/
- Say Hello to selectmenu, a Fully Style-able select Element – CSS-Tricks
- ドキュメント
- selectmenu | Open UI
https://open-ui.org/prototypes/selectmenu
- selectmenu | Open UI
- Chrome Platform Status
- Customizable <select> Element
https://chromestatus.com/feature/5737365999976448
- Customizable <select> Element
- IDL (Chromium)
- chromium/html_select_menu_element.idl at main · chromium/chromium
https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/html/forms/html_select_menu_element.idl
- chromium/html_select_menu_element.idl at main · chromium/chromium
- Issues
- Issues · openui/open-ui –
label:select
https://github.com/openui/open-ui/issues?q=label%3Aselect
- Issues · openui/open-ui –
- デモ
<selectmenu>
tests
https://microsoftedge.github.io/Demos/selectmenu/
デモページでは、フィルター入力付きドロップダウン、複数選択、アイコン付き選択肢などの <selectmenu>
を使った実装が公開されています。複数選択は <selectmenu multiple>
のような属性では実現できず、デモではスクリプトで制御しているようです。
<selectmenu>
使ってみた
ということで早速 <selectmenu>
を使ってみました。 CodeSandbox で esbuild で SolidJS + WindiCSS + Linaria です。
<selectmenu>
Example – CodeSandbox
https://codesandbox.io/s/selectmenu-example-n9tl49?file=/src/SelectFontFamily.tsx
フォントファミリーとフォントサイズの選択に <selectmenu>
を使ってみています。
フォントファミリー選択用のドロップダウンリストは display: grid; grid-template-columns: repeat(3, max-content);
で 3 列のメガメニューにしています。また選択肢の中にサンプルテキストをプレビュー表示しています。 <select>
と違って「ふつうに」スタイリングできました。
<selectmenu
class={css`
user-select: none;
&:focus-within {
outline: auto 3px var(--primary-color);
}
&::part(button) {
cursor: pointer;
font: inherit;
}
&::part(listbox) {
display: grid;
grid-template-columns: repeat(3, max-content);
gap: 3px;
}
`}
onChange={e => props.onChange?.(e.currentTarget.value)}
>
{/* ... <option> ... */}
</selectmenu>
フォントサイズ選択用のドロップダウンリストは <input>
要素を置いてコンボボックスにしています。
<selectmenu
class="tabular-nums"
onChange={e => props.onChange?.(+e.currentTarget.value)}
>
<input
class={/* ... */}
slot="button"
behavior="button"
type="number"
min="10"
value={props.value}
onClick={e => e.currentTarget.focus()}
onChange={e => props.onChange?.(e.currentTarget.valueAsNumber)}
/>
{/* ... <option> ... */}
</selectmenu>
所感等
まだ実験的機能であり、フィードバック募集中とのことで、今後あれこれ変わるかもしれませんが、さらっとさわってみて感じたことをつらつらと書きます。
- ポップアップ制御
- トリガーボタンの押し下げ(pointerdown)でポップアップするのではなく、押して離したとき(click)にポップアップします。
<select>
と異なるので違和感があります。 - トリガーボタンにはトグル動作を期待したのですが、ポップアップ表示中にトリガーボタンをクリックすると、ポインター押下中はポップアップが非表示になり、離すとポップアップが再表示されます。(Issue 立てときました。)
- ポップアップ外の要素をスクロールするとポップアップが閉じてしまいます。これによりライブプレビュー(選択肢のマウスオーバーやフォーカス時点で選択結果をプレビュー表示する)機能の実現が難しくなっています。選択肢をマウスオーバーする (→ コンテンツサイズが変わる → スクロール位置が変わる) → ポップアップが閉じる、という動きになってしまいます。(Issue 立てときました。)
- トリガーボタンの押し下げ(pointerdown)でポップアップするのではなく、押して離したとき(click)にポップアップします。
- キーボード操作
- メガメニューでカーソルキーの動作を直感的にするにはキーボードイベントの独自ハンドリングが必要になると思います。デフォルトでは、ポップアップ上での [↑] キーは 1 つ前、 [↓] キーは 1 つ後ろの選択肢への移動で、メガメニューだと左右への移動になってしまいます。 [←][→] キーは無視されます。さすがに要素の位置関係までは見てくれません。
- ポップアップ非表示かつトリガーボタンフォーカス中、 [↓] キーを押してもポップアップが表示されません。
<select>
と異なるので違和感があります。([Space] キーを押せばポップアップが表示されます。)
- 選択値表示
<select>
のトリガーボタンは選択肢の最大幅に合わせてくれますが、<selectmenu>
はデフォルトでは選択値に応じてトリガーボタンの幅も変わってしまいます。特にインライン配置だと兄弟要素の位置が変わってしまうので、幅の指定はほぼ必須になるかと思います。(overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
あたりもセットかな?)- トリガーボタンに表示される選択値は
<option>
要素のinnerText
です。<option>
要素やその子孫要素が持つ属性はすべて無視され、テキストコンテンツだけが選択値に引き継がれます。(関連 Issue)(Chromium の該当ソースコード)- ポップアップの選択肢には表示して、トリガーボタンの選択値では非表示にしたいテキストは
::before
など擬似要素に入れます。内容によってはアクセシビリティへの配慮が必要かもしれません。 - ポップアップの選択肢では非表示にして、トリガーボタンの選択値には表示したいテキストは
hidden
属性などで非表示にします。
- ポップアップの選択肢には表示して、トリガーボタンの選択値では非表示にしたいテキストは
細かいところが気になっちゃうくらいにはちゃんと使えてますし、もう独自ドロップダウンリストの実装はコリゴリなので、各ブラウザーに実装されて普通に使えるようになるのが待ち遠しいです!
その他の記事
Other Articles
関連職種
Recruit