动画选择器

Web 功能: 可自定义的 <select>

可自定义的 select API 提供了一种声明式、CSS 驱动的方法,可为 <select> 元素及其下拉选择器添加动画效果。通过将 appearance: base-select 与现代 CSS 动画技术(例如 @starting-style 和 allow-discrete 过渡行为)相结合,您可以为顶层元素创建流畅、优质的界面过渡,而无需依赖繁重的 JavaScript 库。

以前,由于原生选择下拉菜单的界面是在可访问的视口约束之外呈现的,因此无法为其添加动画效果。借助 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;
  }
}

策略性实施和最佳实践

  • 务必在需要动画在元素从 display: none 转换为可见时触发时使用 @starting-style。
  • 请勿使用临时滚动锁定。由“base-select”管理的顶层元素应允许自然的背景关闭行为。
  • 务必验证“减少动画”偏好设置。始终将动画约束条件封装在 prefers-reduced-motion 媒体查询中,确保为受晕动症影响的用户提供无障碍环境。
  • 测试布局行为。设置 appearance: base-select 会移除默认浏览器行为,即根据最长选项宽度调整选择框大小。您可能需要设置固定宽度或使用 flex/grid 限制来防止布局偏移。
  • 确保您的 <select> 具有 name 属性和关联的 <label>。这样可以确保即使采用自定义界面,屏幕阅读器也能访问该组件,并且该组件能与标准表单提交功能正常配合使用。
  • 在选项中使用标准 value 属性,以确保表单提交功能与之前完全相同。

后备策略

可自定义 <select> 的回退和浏览器支持

对于尚不支持 appearance: base-select 的浏览器,<select> 元素会优雅降级为标准操作系统下拉菜单。

  • 忽略非文本内容:旧版浏览器会剥离 <option> 标记内的 HTML 标记(例如 <svg> 或 <div>),仅呈现文本节点。确保 <option> 的文本内容本身清晰易读且有意义。
  • HTML 结构处理:标准解析器可能会忽略 <select> 内的 <button> 和 <selectedcontent> 标记,或将其视为无效。如果您将标准文本视为可读的后备内容,则渐进增强功能不需要任何繁重的 JavaScript polyfill。
document.addEventListener("DOMContentLoaded", () => {
  // Check if browser supports base-select value
  if (!CSS.supports("appearance", "base-select")) {
    // Custom select overrides are not supported natively.
  }
});