Animierte Auswahl

Webfunktionen: Anpassbares <select>

Die anpassbare Select API bietet eine deklarative, CSS-gesteuerte Möglichkeit, <select>-Elemente und ihre Drop-down-Auswahl zu animieren. Durch die Kombination von appearance: base-select mit modernen CSS-Animationstechniken wie @starting-style und dem Übergangsverhalten allow-discrete können Sie flüssige, hochwertige UI-Übergänge für Elemente der obersten Ebene erstellen, ohne auf umfangreiche JavaScript-Bibliotheken angewiesen zu sein.

Bisher war es nicht möglich, native Drop-down-Menüs zu animieren, da ihre Benutzeroberfläche außerhalb der zugänglichen Darstellungsbereichsbeschränkungen gerendert wurde. Mit appearance: base-select kann die Auswahl wie jedes andere Seitenelement gestaltet und animiert werden.

Implementierung

So implementieren Sie eine animierte Auswahl:

  1. Anpassung aktivieren: Wenden Sie appearance: base-select sowohl auf das <select>-Element als auch auf das ::picker(select)-Pseudoelement an.
  2. Übergänge mit automatischer Größenanpassung aktivieren (optional): Definieren Sie interpolate-size: allow-keywords (normalerweise für :root), damit der Browser zwischen diskreten Messwerten wie height: auto und height: 0 wechseln kann.
  3. Container der obersten Ebene animieren: Wenden Sie Standardstile für den Ein- und Ausstieg auf ::picker(select) an. Damit der Übergang der Deckkraft beim Wechsel zwischen display: none und display: block funktioniert, müssen Sie transition-behavior: allow-discrete verwenden (oft inline als transition: display 0.3s allow-discrete geschrieben).
  4. Mit @starting-style den Öffnungszustand abfangen: Mit @starting-style definieren Sie die grundlegenden Stile, die der Browser vor Beginn des Übergangs berechnen soll. Wenn Sie beispielsweise möchten, dass es eingeblendet wird, legen Sie die Deckkraft im Block @starting-style auf 0 fest.
  5. Symbol drehen: Verwenden Sie Pseudoelement-Fokus- oder Aktiv-Selektoren wie :open::picker-icon, um Übergänge (z. B. Drehung oder Verschiebung) auf den Pfeil anzuwenden.

Beispielcode: Smooth Select Scale and Fade

Das folgende Beispiel zeigt eine benutzerdefinierte Auswahl, die mit Standardseitenanimationen für den Auswahlcontainer formatiert ist.

<!-- 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;
  }
}

Strategische Implementierung und Best Practices

  • DO use @starting-style when you need animations to trigger exactly when an element transitions from display: none to visible.
  • Verwenden Sie KEINE Ad-hoc-Scrollsperre. Elemente der obersten Ebene, die von „base-select“ verwaltet werden, sollten ein natürliches Schließen des Hintergrunds ermöglichen.
  • DO verify reduced motion preferences. Verpacken Sie Animationsbeschränkungen immer in einer prefers-reduced-motion-Medienabfrage, um eine barrierefreie Umgebung für Personen zu schaffen, die unter Bewegungskrankheit leiden.
  • Layoutverhalten testen Wenn Sie appearance: base-select festlegen, wird das Standardverhalten des Browsers entfernt, bei dem die Größe des Auswahlfelds anhand der Breite der längsten Option festgelegt wird. Möglicherweise müssen Sie eine feste Breite festlegen oder Flex-/Grid-Einschränkungen verwenden, um Layout Shifts zu verhindern.
  • Achte darauf, dass dein <select> ein name-Attribut und ein zugehöriges <label> hat. So bleibt die Komponente auch bei einer benutzerdefinierten UI für Screenreader zugänglich und funktioniert korrekt mit Standardformularübermittlungen.
  • Verwenden Sie standardmäßige value-Attribute für Optionen, damit das Senden von Formularen genau wie zuvor funktioniert.

Fallback-Strategien

Fallbacks und Browserunterstützung für anpassbare <select>

In Browsern, die appearance: base-select noch nicht unterstützen, wird das <select>-Element zu einem Standard-Dropdown-Menü des Betriebssystems herabgestuft.

  • Nicht-Text-Inhalte werden ignoriert: Ältere Browser entfernen HTML-Tags (wie <svg> oder <div>) innerhalb von <option>-Tags und rendern nur die Textknoten. Der Textinhalt des <option>-Elements muss lesbar und für sich genommen aussagekräftig sein.
  • Umgang mit HTML-Strukturen: Standardparser ignorieren möglicherweise die <button>- und <selectedcontent>-Tags innerhalb von <select> oder behandeln sie als ungültig. Für die progressive Verbesserung sind keine umfangreichen JavaScript-Polyfills erforderlich, wenn Sie Standardtext als lesbaren Fallback betrachten.
document.addEventListener("DOMContentLoaded", () => {
  // Check if browser supports base-select value
  if (!CSS.supports("appearance", "base-select")) {
    // Custom select overrides are not supported natively.
  }
});