Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten. Erfahre mehr über dieses Experiment.

View in English Always switch to English

Anpassbare Auswahllisten

Dieser Artikel baut auf dem vorherigen auf und erklärt, wie Sie anpassbare Listbox-<select>-Elemente stylen können.

Ein wesentlicher Vorteil anpassbarer <select>-Listboxen gegenüber „klassischen“ Auswahllisten ist, dass Sie alle Teile des Steuerelements vollständig gestalten können. Darüber hinaus können Sie eine viel größere Vielfalt an Kind-Elementen einfügen, was mehr Flexibilität hinsichtlich Design und Funktionalität bedeutet.

Auswahllisten vs Dropdown-Auswahl

Im vorherigen Artikel haben wir über „Dropdown“-<select>-Elemente gesprochen, bei denen es sich um Steuerelemente handelt, die über einen Knopf verfügen, der beim Drücken eine Dropdown-Auswahl anzeigt, aus der Sie eine Option auswählen können. Diese werden mit einfachem HTML wie <select> spezifiziert.

Im Gegensatz dazu sind „Listbox“-<select>-Elemente Steuerelemente, die eine Box enthalten, die mehrere Optionen gleichzeitig anzeigt, aus denen Sie eine oder mehrere Optionen auswählen können. Sie entscheiden sich für das Rendern einer „Listbox“-Auswahl, indem Sie das multiple-Attribut (um Mehrfachauswahl zu ermöglichen) und/oder einen size-Wert größer als 1 angeben. Zum Beispiel <select multiple> oder <select size="3">.

Das folgende Live-Beispiel verdeutlicht den Unterschied:

Hinweis: Das multiple-Attribut sowie jeder size-Wert größer als 1 versetzen das <select>-Element in den Listbox-Modus.

Wie vergleichen sich anpassbare Listboxen mit anpassbaren Dropdowns?

Eine anpassbare Listbox <select> ist einfacher zu stylen als die Dropdown-Variante:

  • Es gibt keinen Dropdown-Picker, sodass Sie sich nicht darum kümmern müssen, ihn mit dem ::picker(select) Pseudo-Element oder seinen :open- und geschlossenen Zuständen zu stylen.
  • Sie müssen nicht die Ikone des Auswahlknopfes mit ::picker-icon stylen oder steuern, wie die aktuell ausgewählte <option> innerhalb des Knopfes mit dem <selectedcontent>-Element angezeigt wird.
  • Es ist nur ein einziger Container beteiligt; Sie müssen sich nicht um die Position des Pickers im Verhältnis zum Knopf kümmern.

Eine grundlegend angepasste Listbox

Gehen wir ein einfaches Beispiel durch, um zu zeigen, wie eine angepasste Listbox implementiert wird. Das Markup für dieses Beispiel sieht folgendermaßen aus:

html
<p>
  <label for="pet-select">Select pets:</label><br />
  <select id="pet-select" multiple>
    <option value="cat">Cat</option>
    <option value="dog">Dog</option>
    <option value="chicken">Chicken</option>
    <option value="fish">Fish</option>
    <option value="hamster">Hamster</option>
  </select>
</p>

Hier gibt es nichts Besonderes zu beachten. Beachten Sie, dass wir unsere Listbox mit <select multiple> anstelle von <select size="3"> rendern. Der einzige Unterschied ist, dass wir mehrere Optionen anstelle einer einzelnen Option auswählen können. Das Styling funktioniert genau auf die gleiche Weise.

Wir beginnen unser Styling, indem wir das <select> mit einem appearance-Wert von base-select in ein benutzerdefiniertes Styling einbeziehen:

css
select {
  appearance: base-select;
}

Damit können wir unsere <select>- und <option>-Elemente nach Belieben stylen.

Unsere grundlegenden Stile sehen so aus:

css
select {
  border: 2px solid #ddd;
  border-radius: 8px;
  background: #eee;
  width: 200px;
  height: 130px;
}

option {
  background: #eee;
  padding: 10px;
  height: 40px;
  outline: none;
}

option:nth-of-type(odd) {
  background: #fff;
}

Als nächstes setzen wir einen order-Wert von 1 auf das ::checkmark-Pseudo-Element, um das Häkchen für ausgewählte Optionen rechts anstelle von links erscheinen zu lassen, und setzen ein benutzerdefiniertes Häkchen-Symbol mithilfe der content-Eigenschaft.

css
option::checkmark {
  order: 1;
  margin-left: auto;
  content: "☑️";
}

Schließlich setzen wir ein bold font-weight für :checked-Optionen und eine benutzerdefinierte background-Farbe für die Option :hover und :focus-Zustände, damit Sie immer wissen, welche Option Sie schwebend oder fokussiert haben.

css
option:checked {
  font-weight: bold;
}

option:hover,
option:focus {
  background: plum;
}

Dieses Beispiel wird wie folgt wiedergegeben:

Stilvarianten der Listbox

Da angepasste Listboxen nur Standard-HTML-Elemente sind, können Sie sie nach Belieben stylen. In diesem Abschnitt zeigen wir Ihnen ein paar Variationen des vorherigen Beispiels. Beide verwenden das gleiche oder ähnliche Markup; wir haben ein bisschen zusätzliches CSS hinzugefügt, um das Aussehen und Verhalten erheblich zu verändern.

Expanding Listbox

In diesem Beispiel präsentieren wir die Listbox bei der height einer einzelnen Option standardmäßig, indem wir das erzeugte overflow ausblenden und eine transition hinzufügen, um die <select>-Höhe sanft zu animieren, wenn sich ihr Zustand ändert. Wir setzen auch einen interpolate-size-Wert von allow-keywords, damit der Browser zwischen Längen und Schlüsselwörtern animiert.

css
select {
  height: 44px;
  overflow: hidden;
  transition: 0.6s height;
  interpolate-size: allow-keywords;
}

Wir ändern die height auf fit-content, wenn die <select>-Box schwebend oder fokussiert ist, sodass sie sich auf ihre volle Höhe erweitert. Beachten Sie, dass, wenn Sie in eine angepasste Auswahl mit Tabulator gelangen, das erste <option>-Element den Fokus erhält, anstatt das <select> selbst. Daher mussten wir select:has(option:focus) verwenden, um das <select> zu wählen, wenn ein <option> fokussiert ist, anstatt einfach select:focus.

css
select:hover,
select:has(option:focus) {
  height: fit-content;
}

Das Beispiel wird nun so dargestellt:

Horizontale Listbox

In diesem Beispiel präsentieren wir die Listbox-Optionen horizontal anstatt vertikal.

Das HTML ist das gleiche wie in den vorherigen Beispielen, außer dass wir einen zusätzlichen <div>-Wrapper eingefügt haben, um eine width auf das <select> setzen zu können und dann eine andere width auf den Wrapper, sodass alle <option>-Elemente in einer Zeile bleiben und gescrollt werden, wenn das <select> zu schmal wird, um alle aufzunehmen.

html
<p>
  <label for="pet-select">Select pets:</label><br />
  <select id="pet-select" multiple>
    <div class="wrapper">
      <option value="cat">Cat</option>
      <option value="dog">Dog</option>
      <option value="chicken">Chicken</option>
      <option value="fish">Fish</option>
      <option value="hamster">Hamster</option>
      <option value="gerbil">Gerbil</option>
      <option value="guinea">Guinea pig</option>
    </div>
  </select>
</p>

Im CSS setzen wir zunächst die <p>-Element width und margin, sodass das Demo horizontal im Ansichtsfenster zentriert ist und die meiste Breite einnimmt. Anschließend dimensionieren wir das <select> so, dass es die volle Breite seines Elternteils einnimmt und nur so hoch ist wie die <option>-Elemente. Das .wrapper <div> erhält einen display-Wert von flex, wodurch die <option>-Elemente horizontal in einer Reihe angeordnet werden; dann setzen wir seine width, damit sie immer so breit wie die <option>-Elemente ist.

css
p {
  width: 90%;
  margin: 0 auto;
}

select {
  width: 100%;
  height: fit-content;
}

.wrapper {
  display: flex;
  width: fit-content;
}

Als nächstes geben wir den <option>-Elementen etwas zusätzlichen Abstand, um sie horizontal zu trennen, und einen position-Wert von relativ, damit wir ihre Nachkommen relativ zu ihnen positionieren können.

css
option {
  padding: 10px 30px;
  position: relative;
}

Schließlich positionieren wir die Optionshäkchen absolut und geben ihnen ein benutzerdefiniertes Aussehen.

css
option::checkmark {
  position: absolute;
  top: -2px;
  left: 2px;
  font-size: 1.5rem;
  color: red;
  text-shadow: 1px 1px 1px black;
}

Unsere zweite Variante wird folgendermaßen gerendert:

Eine komplexere Listbox

In diesem Abschnitt gehen wir ein komplexeres Beispiel durch, das eine Kontaktpicker-Listbox mit einem integrierten Filterfeld und einem Link zum Aufrufen eines (fiktiven) Kontaktbearbeitungsmodus bietet.

HTML

Im Markup fügen wir ein <form> ein, das eine Überschrift und einen Wrapper-<div> enthält. Innerhalb des Wrappers fügen wir drei weitere <div>-Elemente ein, die jeweils ein Text-<input> enthalten, das unser Filterfeld darstellt, eine Listbox <select> und einen Link. Das <select> wird mit <option>-Elementen bestückt, die unsere Kontaktmöglichkeiten darstellen, und zwar über JavaScript.

html
<form>
  <h2>Contact select</h2>
  <div class="wrapper">
    <div class="filter">
      <input
        type="text"
        aria-label="Filter contacts"
        placeholder="Filter by name, e.g. amara" />
    </div>
    <div class="options">
      <select
        multiple
        name="contact-select"
        aria-label="Select contacts"></select>
    </div>
    <div class="edit">
      <a href="#">Edit contacts</a>
    </div>
  </div>
</form>

CSS

Wir beginnen unser CSS, indem wir das <select>-Element wie zuvor in ein benutzerdefiniertes Styling einbeziehen:

css
select {
  appearance: base-select;
}

Der Großteil des Stylings ist ziemlich einfach, aber wir gehen es durch und weisen dabei auf alles Wesentliche hin. Zuerst stylen wir das .wrapper <div>, indem wir ihm eine feste width zuweisen, die die horizontale Größe des gesamten Steuerelements bestimmt.

css
.wrapper {
  border: 2px solid #ddd;
  border-radius: 8px;
  background: #ddd;
  width: 250px;
}

Als nächstes stylen wir das Filter<input>, das .options <div> und das enthaltene <select>, sowie das .edit <div>, das den Link enthält. Am bemerkenswertesten ist, dass wir dem <select> eine feste height und einen overflow-y-Wert von scroll zuweisen, damit die enthaltenen <option>-Elemente darin scrollen.

css
.filter input {
  display: block;
  padding: 5px;
  border-radius: 5px;
  border: 1px solid #bbb;
  width: 95%;
  margin: 8px auto;
}

.options {
  padding: 0 5px;
  background: #ddd;
}

select {
  height: 200px;
  overflow-y: scroll;
  width: 100%;
  border: 1px solid #bbb;
}

.edit {
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
}

Wir stylen unsere <option>-Elemente ähnlich wie in früheren Beispielen, geben ihnen eine Zebrastreifen-Optik sowie klare :hover- und :focus-Stile:

css
option {
  background: #eee;
  padding: 10px;
}

option:nth-of-type(odd) {
  background: #fff;
}

option:checked {
  font-weight: bold;
}

option:hover,
option:focus {
  background: plum;
}

Unser nächster Schritt ist, das standardmäßige Fokus-Outlines für die <input>, <option>, und <a>-Elemente zu entfernen. Wir haben in dem vorherigen Codeblock bereits alternative Stile für die <option>-Elemente bereitgestellt; hier bieten wir subtilere Alternativen für die <input>- und <a>-Elemente.

css
input,
option,
a {
  outline: none;
}

input:hover,
input:focus {
  border: 1px solid #999;
  background: #eef;
}

.edit a {
  color: #333;
}

a:hover,
a:focus {
  outline: 2px dotted #666;
}

Schließlich bieten wir benutzerdefinierte Stile für die Häkchen der ausgewählten Optionen über das ::checkmark Pseudo-Element:

css
option::checkmark {
  order: 1;
  margin-left: auto;
  content: "☑️";
}

JavaScript

Die letzte Ergänzung, die unser Beispiel benötigt, ist etwas JavaScript, um die Funktionalität zur Bestückung und Filterung der Optionen zu ermöglichen.

Auf einer echten Website ziehen Sie wahrscheinlich eine aktuelle Kontaktliste von einem Server ein, aber in diesem Fall haben wir die Daten in einem statischen contacts-Objekt bereitgestellt (wir haben die meisten Kontakte aus Platzgründen ausgeblendet). Für jeden Kontakt speichern wir einen Namen und ein booleanes Feld, das angibt, ob sie im <select>-Element ausgewählt wurden.

js
const contacts = [
  { name: "Aisha Khan", selected: false },
  // …
];

Wir beginnen damit, Verweise auf unsere .filter <input> und <select>-Elemente zu sammeln:

js
const filterInput = document.querySelector(".filter input");
const select = document.querySelector("select");

Als nächstes definieren wir eine Funktion namens populateOptions(), die ein Array von Objekten als Parameter nimmt. Innerhalb der Funktion leeren wir zuerst den Inhalt des <select>-Elements. Dann durchlaufen wir das Eingabearray und erstellen ein <option>-Element für jedes Objekt im Array, indem wir seine textContent- und selected-Eigenschaften auf die name- und selected-Eigenschaften des Objekts setzen. Jedes <option>-Element wird dem DOM als Kind des <select> hinzugefügt.

js
function populateOptions(array) {
  select.innerHTML = "";

  array.forEach((obj) => {
    const option = document.createElement("option");
    option.textContent = obj.name;
    option.selected = obj.selected;
    select.appendChild(option);
  });
}

Jetzt definieren wir eine weitere Funktion, filterOptions(), die einen Filter-String und ein Array von Objekten als Parameter nimmt. Wir prüfen, ob der String gleich dem leeren String oder einem oder mehreren Leerzeichen entspricht, indem wir den Rückgabewert seiner trim()-Methode mit "" vergleichen. Wenn dies true ergibt, führen wir die populateOptions()-Funktion aus und übergeben ihr das gesamte Array, sodass das <select> mit allen <option>-Elementen bestückt wird. Wenn false zurückgegeben wird, filtern wir das Eingabearray mit seiner filter()-Methode um nur Objekte einzuschließen, deren name-Eigenschaft mit dem filter-String startsWith() beginnt, und übergeben dann das gefilterte Array an die populateOptions()-Funktion, sodass das <select> mit einem gefilterten Satz von <option>-Elementen bestückt wird.

js
function filterOptions(filter, array) {
  if (filter.trim() === "") {
    populateOptions(array);
  } else {
    const filteredArray = array.filter((obj) =>
      obj.name.toLowerCase().startsWith(filter.toLowerCase()),
    );
    populateOptions(filteredArray);
  }
}

Hinweis: Wir konvertieren sowohl den Objektnamen als auch den filter-String in Kleinbuchstaben mit toLowerCase(), damit die Filterübereinstimmung nicht zwischen Groß- und Kleinschreibung unterscheidet.

Als nächstes fügen wir einen input-Eventlistener zum .filter <input>-Element hinzu, sodass bei jeder Bearbeitung seines Werts die filterOptions()-Funktion ausgeführt wird, um die angezeigten <option>-Elemente zu filtern. Wir übergeben den aktuellen Wert des <input> als Filter-String und das contacts-Array als Eingabearray.

js
filterInput.addEventListener("input", () => {
  filterOptions(filterInput.value, contacts);
});

Der nächste Codeabschnitt fügt einen change-Eventlistener zum <select>-Element hinzu, sodass bei jeder Auswahl oder Deselektion eines <option> die ausgewählten Status der Objekte im contacts-Array mit dem ausgewählten Status der derzeit angezeigten <option>-Objekte synchronisiert werden. Dies ist erforderlich, weil jedes Mal, wenn wir einen neuen Filter auf unser <select>-Element anwenden, die angezeigten <option>-Elemente frisch aus dem contacts-Array generiert werden, einschließlich ihres ausgewählten Status. Wenn wir dies nicht täten, würden wir unsere ausgewählten Optionen jedes Mal verlieren, wenn wir den Filter ändern.

Es gibt keine Möglichkeit, genau zu erkennen, welche <option> jedes Mal geändert wurde, wenn eine umgeschaltet wird. Daher haben wir das Problem wie folgt gelöst:

  1. Holen Sie sich ein Array aller derzeit angezeigten <option>-Werte, indem Sie ein Array aus der select.options-Sammlung mit Array.from erstellen und es dann mit seiner map()-Methode abbilden, um jedes <option> im Array durch seinen Wert zu ersetzen.
  2. Holen Sie sich ein Array aller derzeit ausgewählten <option>-Werte mithilfe der gleichen Methodik, außer dass wir dieses Mal das Eingabearray aus der select.selectedOptions-Sammlung erstellen.
  3. Für jedes Kontaktobjekt im contacts-Array prüfen wir, ob der Kontakt name-Eigenschaftswert im allCurrentValues-Array mit der includes()-Methode enthalten ist. Wenn nicht, ignorieren wir es, damit wir nicht den Status der ausgewählten Kontaktobjekte umschalten, die nicht einmal angezeigt werden. Wenn ja, setzen wir die selected-Eigenschaft des Kontakts auf das Ergebnis der Überprüfung, ob das currentSelectedValues-Array den Namen des Kontakts includes() enthält - wenn dies der Fall ist, setzen wir die Objekt-Eigenschaft auf true, sonst auf false.
js
select.addEventListener("change", () => {
  const allCurrentValues = Array.from(select.options).map(
    (option) => option.value,
  );
  const currentSelectedValues = Array.from(select.selectedOptions).map(
    (option) => option.value,
  );

  contacts.forEach((contact) => {
    if (allCurrentValues.includes(contact.name)) {
      contact.selected = currentSelectedValues.includes(contact.name);
    }
  });
});

Schließlich führen wir die populateOptions()-Funktion aus und übergeben ihr das contacts-Array, sodass beim Laden der Seite die vollständige Liste der Kontakte angezeigt wird.

js
populateOptions(contacts);

Ergebnis

Das Beispiel wird wie folgt gerendert:

Als Nächstes

Im nächsten Artikel dieses Moduls werden wir die verschiedenen UI-Pseudoklassen erkunden, die uns in modernen Browsern zur Verfügung stehen, um Formulare in verschiedenen Zuständen zu gestalten.

Siehe auch