動畫選取挑選器

網頁功能: 可自訂的 <select>

可自訂的選取 API 提供宣告式、CSS 驅動的方式,為 <select> 元素及其下拉式挑選器建立動畫。只要將 appearance: base-select 與現代 CSS 動畫技術 (例如 @starting-style 和 allow-discrete 轉場行為) 結合,就能為頂層元素建立流暢、優質的 UI 轉場效果,不必依賴龐大的 JavaScript 程式庫。

過去,原生選取下拉式選單的 UI 是在可存取的檢視區限制之外算繪,因此無法建立動畫。有了 appearance: base-select,挑選器就能像其他網頁元素一樣設定樣式和動畫。

導入方式

如要導入動畫選取挑選器,請按照下列步驟操作:

  1. 選擇加入自訂功能:將 appearance: base-select 同時套用至 <select> 元素和 ::picker(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;
  }
}

策略執行與最佳做法

  • 務必使用 @starting-style,在元素從 display: none 轉換為可見時,準確觸發動畫。
  • 請勿使用臨時捲動鎖定。由「base-select」管理的頂層元素應允許自然關閉背景幕。
  • 務必確認使用者是否偏好減少動態效果。請務必將動畫限制包裝在 prefers-reduced-motion 媒體查詢中,確保受動暈症影響的使用者能享有友善的無障礙環境。
  • 測試版面配置行為。設定 appearance: base-select 會移除預設的瀏覽器行為,也就是根據最長的選項寬度調整選取範圍大小。您可能需要設定固定寬度,或使用彈性/格線限制,避免版面配置位移。
  • 確認 <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.
  }
});