Anpassbare Auswahllisten
Dieser Artikel knüpft an den vorherigen an und betrachtet, wie anpassbare Listbox-<select>-Elemente gestaltet werden können.
Ein großer Vorteil von anpassbaren <select>-Listenboxen im Vergleich zu "klassischen" Auswahllisten besteht darin, dass Sie alle Teile des Steuerelements vollständig gestalten können. Außerdem können Sie eine viel breitere Vielfalt von untergeordneten Elementen in ihnen enthalten, was eine größere Flexibilität in Bezug auf Design und Funktionalität bedeutet.
Auswahllisten vs. Dropdown-Selects
Im vorherigen Artikel haben wir über "Dropdown"-<select>-Elemente gesprochen, bei denen es sich um Steuerelemente handelt, die über eine Schaltfläche verfügen, die beim Drücken einen Dropdown-Auswahler öffnet, aus dem Sie eine Option auswählen können. Diese werden mithilfe von einfachem HTML wie <select> festgelegt.
Im Gegensatz dazu sind "Listbox"-<select>-Elemente Steuerelemente, die eine Box anzeigen, in der mehrere Optionen gleichzeitig angezeigt werden, aus denen Sie eine oder mehrere Optionen auswählen können. Sie entscheiden sich für das Rendern einer "Listbox"-Select, indem Sie das Attribut multiple angeben (um Mehrfachauswahl zu ermöglichen) und/oder einen size-Wert größer als 1. 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 versetzt das <select>-Element in den Listbox-Modus.
Wie vergleichen sich anpassbare Listboxen mit anpassbaren Dropdowns?
Eine anpassbare Listbox-<select> ist einfacher zu gestalten als die Dropdown-Variante:
- Es gibt keinen Dropdown-Auswahler, sodass Sie sich nicht darum kümmern müssen, ihn mit dem
::picker(select)Pseudo-Element oder seinen:openund geschlossenen Zuständen zu gestalten. - Sie müssen sich nicht darum kümmern, das Symbol der Select-Schaltfläche mit
::picker-iconzu gestalten oder zu manipulieren, wie der derzeit ausgewählte<option>innerhalb der Schaltfläche mit dem<selectedcontent>-Element angezeigt wird. - Es ist nur ein einzelner Container beteiligt; Sie müssen sich nicht um die Position des Auswählers relativ zur Schaltfläche kümmern.
Eine grundlegende angepasste Listbox
Lassen Sie uns durch ein grundlegendes Beispiel gehen, um zu zeigen, wie eine angepasste Listbox implementiert wird. Das Markup für dieses Beispiel sieht folgendermaßen aus:
<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 Bemerkenswertes. Beachten Sie, dass wir unsere Listbox mit <select multiple> anstelle von <select size="3"> rendern. Der einzige Unterschied besteht darin, dass wir mehrere Optionen anstelle einer einzelnen Option auswählen können. Die Gestaltung funktioniert genau auf die gleiche Weise.
Wir beginnen unsere Gestaltung, indem wir das <select> mit einem appearance-Wert von base-select in eine benutzerdefinierte Gestaltung bringen:
select {
appearance: base-select;
}
Damit können wir jetzt unsere <select>- und <option>-Elemente nach Belieben gestalten.
Unsere grundlegenden Stile sehen folgendermaßen aus:
select {
border: 2px solid #dddddd;
border-radius: 8px;
background: #eeeeee;
width: 200px;
height: 130px;
}
option {
background: #eeeeee;
padding: 10px;
height: 40px;
outline: none;
}
option:nth-of-type(odd) {
background: white;
}
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äkchensymbol mit der content-Eigenschaft.
option::checkmark {
order: 1;
margin-left: auto;
content: "☑️";
}
Zum Schluss setzen wir einen bolden font-weight auf :checked Optionen und eine benutzerdefinierte background-Farbe für die Option :hover und :focus-Zustände, sodass Sie immer wissen, welche Option Sie gehoben oder fokussiert haben.
option:checked {
font-weight: bold;
}
option:hover,
option:focus {
background: plum;
}
Dieses Beispiel rendert folgendermaßen:
Listbox-Stilvariationen
Da angepasste Listboxen nur Standard-HTML-Elemente sind, können Sie diese nach Belieben gestalten. In diesem Abschnitt zeigen wir Ihnen einige Variationen des vorherigen Beispiels. Sie verwenden alle dasselbe oder ein ähnliches Markup; wir haben ein wenig zusätzliches CSS hinzugefügt, um das Aussehen und Gefühl erheblich zu verändern.
Erweiterbare Listbox
In diesem Beispiel präsentieren wir die Listbox standardmäßig in der height einer einzelnen Option, wobei der dadurch erzeugte overflow versteckt wird, und fügen eine transition hinzu, um die <select>-Höhe bei einer Zustandsänderung sanft zu animieren. Außerdem setzen wir einen interpolate-size-Wert von allow-keywords, um den Browser in die Animation zwischen Längen und Schlüsselwörtern einzubinden.
select {
height: 44px;
overflow: hidden;
transition: 0.6s height;
interpolate-size: allow-keywords;
}
Wir ändern die height in fit-content, wenn die <select> gehoben oder fokussiert wird, sodass sie sich zu ihrer vollen Höhe erstreckt. Beachten Sie, dass beim Tab-Wechsel in ein angepasste Select das erste <option> den Fokus erhält anstelle des <select> selbst. Daher mussten wir select:has(option:focus) verwenden, um das <select> auszuwählen, wenn ein <option> fokussiert ist, anstatt nur select:focus.
select:hover,
select:has(option:focus) {
height: fit-content;
}
Das Beispiel rendert nun so:
Horizontale Listbox
In diesem Beispiel stellen wir die Listbox-Optionen horizontal statt vertikal dar.
Das HTML ist das gleiche wie bei den vorherigen Beispielen, außer dass wir einen zusätzlichen Wrapper-<div> eingefügt haben, um eine width auf dem <select> festzulegen und dann eine andere width auf dem Wrapper, sodass alle <option>-Elemente in einer Linie gehalten und gescrollt werden können, wenn das <select> zu schmal wird, um sie alle unterzubringen.
<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 beginnen wir damit, das umgebende <p>-Element zu sizeieren und mit width und margin zu versehen, damit das Demo horizontal im Ansichtsfenster zentriert wird und den größten Teil der Breite einnimmt. Wir dimensionieren dann das <select>, um die volle Breite des übergeordneten Elements einzunehmen und nur so groß zu sein wie die <option>-Elemente. Dem .wrapper <div> wird ein display-Wert von flex zugewiesen, wodurch die <option>-Elemente horizontal in einer Reihe angeordnet werden; wir setzen dann seine width, damit es immer so breit wie die <option>-Elemente ist.
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 verteilen, und einen position-Wert von relativ, damit wir ihre Nachkommen relativ zu ihnen positionieren können.
option {
padding: 10px 30px;
position: relative;
}
Schließlich positionieren wir die Option-Häkchen absolut und geben ihnen ein benutzerdefiniertes Erscheinungsbild.
option::checkmark {
position: absolute;
top: -2px;
left: 2px;
font-size: 1.5rem;
color: red;
text-shadow: 1px 1px 1px black;
}
Unsere zweite Variation rendert folgendermaßen:
Eine komplexere Listbox
In diesem Abschnitt gehen wir durch ein komplexeres Beispiel, das eine Kontakt-Wähler-Listbox mit einem integrierten Filterfeld und einem Link zum Zugreifen auf einen (fiktiven) Kontaktbearbeitungsmodus bereitstellt.
HTML
Im Markup enthalten wir ein <form>, das eine Überschrift und einen umschließenden <div> enthält. Im Umschließer sind drei weitere <div>-Elemente enthalten, die jeweils ein Text-<input> darstellen, das unser Filterfeld repräsentiert, eine Listbox-<select> und einen Link. Das <select> wird mit <option>-Elementen gefüllt, die unsere Kontaktoptionen über JavaScript darstellen.
<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 in benutzerdefinierte Gestaltung bringen, wie zuvor:
select {
appearance: base-select;
}
Der Großteil des Stylings ist recht einfach, aber wir gehen es durch und weisen auf alles Wichtige hin. Zuerst gestalten wir das .wrapper <div>, indem wir ihm eine feste width geben, die die horizontale Größe des gesamten Steuerelements kontrolliert.
.wrapper {
border: 2px solid #dddddd;
border-radius: 8px;
background: #dddddd;
width: 250px;
}
Als nächstes gestalten wir das Filter-<input>, das .options <div> und das enthaltene <select> und das .edit <div>, das den Link enthält. Besonders bemerkenswert ist, dass wir dem <select> eine feste height und einen overflow-y-Wert von scroll geben, sodass die enthaltenen <option>-Elemente darin scrollen.
.filter input {
display: block;
padding: 5px;
border-radius: 5px;
border: 1px solid #bbbbbb;
width: 95%;
margin: 8px auto;
}
.options {
padding: 0 5px;
background: #dddddd;
}
select {
height: 200px;
overflow-y: scroll;
width: 100%;
border: 1px solid #bbbbbb;
}
.edit {
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
Wir gestalten unsere <option>-Elemente ähnlich wie frühere Beispiele, indem wir ihnen Zebrastreifen geben und klare :hover- und :focus-Stile anwenden:
option {
background: #eeeeee;
padding: 10px;
}
option:nth-of-type(odd) {
background: white;
}
option:checked {
font-weight: bold;
}
option:hover,
option:focus {
background: plum;
}
Unser nächster Schritt ist es, den Standard-Fokusumriss für die <input>, <option>, und <a> Elemente zu entfernen. Wir haben bereits alternative Stile für die <option>-Elemente im vorherigen Codeblock bereitgestellt; hier bieten wir subtilere Alternativen für die <input> und <a>-Elemente.
input,
option,
a {
outline: none;
}
input:hover,
input:focus {
border: 1px solid #999999;
background: #eeeeff;
}
.edit a {
color: #333333;
}
a:hover,
a:focus {
outline: 2px dotted #666666;
}
Zum Schluss bieten wir benutzerdefinierte Stile für die Häkchen der ausgewählten Optionen über das ::checkmark-Pseudo-Element:
option::checkmark {
order: 1;
margin-left: auto;
content: "☑️";
}
JavaScript
Die letzte Ergänzung, die unser Beispiel benötigt, ist etwas JavaScript, um die Optionen zu füllen und die Filterfunktion bereitzustellen.
Auf einer echten Website würden Sie wahrscheinlich eine aktuelle Kontaktliste von einem Server abrufen, aber in diesem Fall haben wir die Daten in einem statischen contacts-Objekt bereitgestellt (wir haben die meisten der Kontakte aus Gründen der Kürze versteckt). Für jeden Kontakt speichern wir einen Namen und ein Boolean, das anzeigt, ob dieser im <select>-Element ausgewählt wurde.
const contacts = [
{ name: "Aisha Khan", selected: false },
// …
];
Wir beginnen damit, Referenzen auf unsere .filter-<input> und <select>-Elemente zu ermitteln:
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. In 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, wobei wir dessen textContent und selected-Eigenschaften auf die des Objekts name und selected setzen. Jedes <option>-Element wird dem DOM als Kind des <select> hinzugefügt.
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 Filterstring und ein Array von Objekten als Parameter nimmt. Wir überprüfen, ob der String mit dem leeren String oder einem oder mehreren Leerzeichen übereinstimmt, indem wir den Rückgabewert seiner trim()-Methode mit "" vergleichen. Wenn dies true zurückgibt, führen wir die populateOptions()-Funktion aus, indem wir ihr das vollständige Array übergeben, damit das <select> mit allen <option>-Elementen gefüllt wird. Wenn es false zurückgibt, filtern wir das Eingabearray mit seiner filter()-Methode, um nur Objekte einzuschließen, deren name-Eigenschaft den filter-String startsWith(), und dann übergeben wir das gefilterte Array an die populateOptions()-Funktion, damit das <select> mit einer gefilterten Menge von <option>-Elementen gefüllt wird.
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 wandeln sowohl den Objekt-name als auch den filter-String mit toLowerCase() in Kleinbuchstaben um, damit das Filter-Matching nicht zwischen Groß- und Kleinschreibung unterscheidet.
Als nächstes fügen wir dem .filter-<input>-Element einen input-Eventlistener hinzu, sodass, wenn sein Wert bearbeitet wird, die filterOptions()-Funktion aufgerufen wird, um die angezeigten <option>-Elemente zu filtern. Wir übergeben ihm den aktuellen Wert des <input> als Filterstring und das contacts-Array als Eingabearray.
filterInput.addEventListener("input", () => {
filterOptions(filterInput.value, contacts);
});
Der nächste Codeabschnitt fügt dem <select>-Element einen change-Eventlistener hinzu, sodass jedes Mal, wenn ein <option> ausgewählt oder abgewählt wird, der selected-Status der Objekte im contacts-Array mit dem ausgewählten Status der derzeit angezeigten <option>-Objekte synchronisiert wird. Dies ist erforderlich, da jedes Mal, wenn wir einen neuen Filter auf unser <select>-Element anwenden, die angezeigten <option>-Elemente frisch aus dem contacts-Array generiert werden, welches ihren ausgewählten Zustand beinhaltet. 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, welches <option> jedes Mal geändert wurde, wenn eines umgeschaltet wird. Wir haben das Problem folgendermaßen gelöst:
- Holen Sie sich ein Array aller derzeit angezeigten
<option>-Werte, indem Sie ein Array aus derselect.options-Sammlung mitArray.fromerstellen und es dann mit seinermap()-Methode abbilden, um jede<option>im Array durch ihren Wert zu ersetzen. - Holen Sie sich ein Array aller derzeit ausgewählten
<option>-Werte mit derselben Methodik, außer dass wir diesmal das Eingabearray aus derselect.selectedOptions-Sammlung erstellen. - Für jedes Kontaktobjekt im
contacts-Array prüfen Sie, ob der Kontakt-name-Eigenschaftswert imallCurrentValues-Array mithilfe derincludes()-Methode enthalten ist. Falls nicht, ignorieren Sie es, damit wir nicht den ausgewählten Status von Kontakten umschalten, die nicht einmal angezeigt werden. Falls ja, setzen Sie dieselected-Eigenschaft des Kontakts auf das Ergebnis der Überprüfung, ob dascurrentSelectedValues-Array den Kontaktnameincludes()- wenn dies der Fall ist, setzen Sie die Objekteigenschaft auftrue, andernfalls auffalse.
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);
}
});
});
Zum Schluss führen wir die populateOptions()-Funktion aus und übergeben ihr das contacts-Array, damit beim Laden der Seite die vollständige Liste der Kontakte angezeigt wird.
populateOptions(contacts);
Ergebnis
Das Beispiel rendert folgendermaßen:
Als Nächstes
Im nächsten Artikel dieses Moduls werden wir die verschiedenen UI-Pseudoklassen untersuchen, die uns in modernen Browsern zur Verfügung stehen, um Formulare in verschiedenen Zuständen zu gestalten.