アニメーション選択ピッカー

カスタマイズ可能な select API は、<select> 要素とそのプルダウン選択ツールをアニメーション化する、CSS 駆動の宣言的な方法を提供します。appearance: base-select と最新の CSS アニメーション手法(@starting-style や allow-discrete 遷移動作など)を組み合わせることで、重い JavaScript ライブラリに依存することなく、最上位レイヤの要素に流動的でプレミアムな UI 遷移を作成できます。

以前は、ネイティブの選択プルダウンをアニメーション化することはできませんでした。これは、UI がアクセス可能なビューポートの制約外でレンダリングされていたためです。appearance: base-select を使用すると、選択ツールは他のページ要素と同様にスタイル設定やアニメーション化が可能になります。

実装方法

アニメーション付きの選択ピッカーを実装するには:

  1. カスタマイズを有効にする: <select> 要素と ::picker(select) 疑似要素の両方に appearance: base-select を適用します。
  2. 自動サイズ調整の遷移を有効にする(省略可): interpolate-size: allow-keywords(通常は :root)を定義して、ブラウザが height: auto や height: 0 などの個別の指標値の間で遷移できるようにします。
  3. 最上位レイヤのコンテナをアニメーション化する: ::picker(select) に標準のエントリ / 終了スタイルを適用します。display: none と display: block の間で移動するときに不透明度の遷移が機能するようにするには、transition-behavior: allow-discrete(多くの場合、インラインで transition: display 0.3s allow-discrete と記述されます)を使用する必要があります。
  4. @starting-style を使用して開いた状態にフックする: @starting-style を使用して、ブラウザが遷移の開始前に計算する必要があるベースライン スタイルを定義します。たとえば、フェードインする場合は、@starting-style ブロック内で不透明度を 0 に設定します。
  5. アイコンを回転させる: :open::picker-icon などの疑似要素のフォーカス セレクタまたはアクティブ セレクタを使用して、矢印インジケーターに遷移(回転や移動など)を適用します。

コード例: スムーズな選択、拡大縮小、フェード

次の例は、選択コンテナに標準のページ アニメーションを適用したカスタム選択スタイルを示しています。

<!-- Always use a <label> linked via 'for' to the select for accessibility -->
<label for="theme-select">Visual Theme</label>
<select id="theme-select" class="animated-select" name="theme">
  <!-- The <button> inside <select> becomes the visible trigger when appearance: base-select is used -->
  <button>
    <!-- <selectedcontent> automatically displays the content of the chosen <option> -->
    <selectedcontent></selectedcontent>
  </button>
  <option value="system">
    <!-- MANDATORY: Decorative inline SVGs MUST set aria-hidden="true" to prevent redundant screen reader announcement -->
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
      <line x1="8" y1="21" x2="16" y2="21"></line>
      <line x1="12" y1="17" x2="12" y2="21"></line>
    </svg>
    System Default
  </option>
  <option value="light">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <circle cx="12" cy="12" r="5"></circle>
      <line x1="12" y1="1" x2="12" y2="3"></line>
      <line x1="12" y1="21" x2="12" y2="23"></line>
      <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
      <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
      <line x1="1" y1="12" x2="3" y2="12"></line>
      <line x1="21" y1="12" x2="23" y2="12"></line>
      <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
      <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
    </svg>
    Light UI
  </option>
</select>
/* Opt-in to customizable select */
.animated-select,
.animated-select::picker(select) {
  appearance: base-select;
}

/* Enable auto-keyword transitions (usually set globally at :root) */
:root {
  interpolate-size: allow-keywords;
}

/* Style the visible trigger and icon rotation */
.animated-select {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0.875rem 1rem;
  font-size: 1rem;
  border-radius: 8px;
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.animated-select::picker-icon {
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.animated-select:open::picker-icon {
  transform: rotate(180deg);
}

/*
 * The Picker Container
 * Uses top-layer animations with `allow-discrete` visibility hooks
 */
.animated-select::picker(select) {
  background: white;
  border-radius: 12px;
  box-shadow: 0 10px 25px -3px rgba(0,0,0,0.1);
  padding: 0.5rem;
  margin-top: 0.25rem;
  width: anchor-size(width);
  overflow: hidden;

  /* The crucial transition setting for popover animations */
  transition:
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete,
    opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  opacity: 0;
  height: 0;
}

/* Open State */
.animated-select:open::picker(select) {
  opacity: 1;
  height: auto;
}

/* @starting-style to hook the transition on initial popover open */
@starting-style {
  .animated-select:open::picker(select) {
    opacity: 0;
    height: 0;
  }
}

/* Support for SVG inside Options and Selected Content */
.animated-select option svg,
.animated-select selectedcontent svg {
  flex-shrink: 0; /* Prevent icons from shrinking */
  width: 1.25rem;
  height: 1.25rem;
}

/* MANDATORY: Provide multiple indicators (e.g. bold font and distinct background) for the checked state to avoid color-only state communication */
.animated-select option:checked {
  font-weight: 700;
  background-color: #f1f5f9;
}

/* Ensure copy-paste safety for users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
  .animated-select::picker(select),
  .animated-select::picker-icon {
    transition: none !important;
  }
}

戦略的な実装とベスト プラクティス

  • 要素が display: none から表示に遷移するタイミングでアニメーションをトリガーする必要がある場合は、@starting-style を使用します。
  • アドホックなスクロール ロックは使用しないでください。「base-select」で管理される最上位レイヤの要素は、自然なバックドロップの閉じ動作を可能にする必要があります。
  • 推奨: 視覚効果を減らす設定を確認します。乗り物酔いの影響を受けるユーザーがアクセスしやすい環境を確保するため、アニメーションの制約は常に prefers-reduced-motion メディアクエリでラップしてください。
  • レイアウトの動作をテストします。appearance: base-select を設定すると、最も長いオプションの幅に基づいて選択要素のサイズを決定するデフォルトのブラウザ動作が削除されます。レイアウト シフトを防ぐために、固定幅を設定するか、flex / grid 制約を使用する必要がある場合があります。
  • <select> に name 属性と関連付けられた <label> があることを確認します。これにより、カスタム UI を使用している場合でも、コンポーネントはスクリーン リーダーにアクセス可能であり、標準のフォーム送信で正しく動作します。
  • オプションで標準の value 属性を使用して、フォームの送信が以前とまったく同じように機能するようにします。

代替戦略

カスタマイズ可能な <select> のフォールバックとブラウザ サポート

appearance: base-select をまだサポートしていないブラウザでは、<select> 要素は標準のオペレーティング システムのプルダウンに適切にフォールバックされます。

  • テキスト以外のコンテンツが無視される: 古いブラウザでは、<option> タグ内の HTML タグ(<svg> や <div> など)が削除され、テキストノードのみがレンダリングされます。<option> のテキスト コンテンツが、それ自体で読みやすく、意味のあるものになるようにします。
  • HTML 構造の処理: 標準のパーサーは、<select> 内の <button> タグと <selectedcontent> タグを無視するか、無効として扱うことがあります。標準テキストを読み取り可能なフォールバックとして表示する場合、プログレッシブ エンハンスメントに重い JavaScript ポリフィルは厳密には必要ありません。
document.addEventListener("DOMContentLoaded", () => {
  // Check if browser supports base-select value
  if (!CSS.supports("appearance", "base-select")) {
    // Custom select overrides are not supported natively.
  }
});