Anpassbare Auswahl-Listenfelder
Dieser Artikel knüpft an den vorherigen an und zeigt, wie anpassbare Listenfeld-<select>-Elemente gestaltet werden können.
Ein wesentlicher Vorteil von anpassbaren <select>-Listenfeldern gegenüber „klassischen“ Select-Listenfeldern ist, dass Sie alle Teile des Steuerungselements vollständig gestalten können und eine viel größere Vielfalt an Kindelementen darin aufnehmen können, was eine größere Flexibilität bei Design und Funktionalität bedeutet.
Select-Listenfelder 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 ein Dropdown-Auswahlfenster anzeigt, aus dem Sie eine Option auswählen können. Diese werden mit grundlegenden HTML-Tags wie <select> angegeben.
Im Gegensatz dazu sind „Listenfeld“-<select>-Elemente Steuerelemente mit einem Feld, das mehrere Optionen gleichzeitig anzeigt, aus denen Sie eine oder mehrere Optionen auswählen können. Sie entscheiden sich für die Darstellung eines „Listenfeld“-Selects, 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 veranschaulicht den Unterschied:
Hinweis:
Das multiple-Attribut sowie jeder size-Wert größer als 1 versetzen das <select>-Element in den Listenfeldmodus.
Wie vergleichen sich anpassbare Listenfelder mit anpassbaren Dropdowns?
Ein anpassbares Listenfeld-<select> ist einfacher zu gestalten als die Dropdown-Variante:
- Es gibt keinen Dropdown-Auswähler, sodass Sie sich nicht um die Gestaltung mit dem
::picker(select)-Pseudoelement oder dessen:open- und Geschlossen-Zustände kümmern müssen. - Sie müssen sich nicht um die Gestaltung des Symbols der Auswahlschaltfläche mit
::picker-iconkümmern oder um die Manipulation, wie das aktuell ausgewählte<option>innerhalb der Schaltfläche mit dem<selectedcontent>-Element angezeigt wird. - Es gibt nur einen einzigen Container; Sie müssen sich nicht um die Position des Auswählers relativ zur Schaltfläche kümmern.
Ein einfaches angepasstes Listenfeld
Durchgehen wir ein einfaches Beispiel, um zu zeigen, wie ein angepasstes Listenfeld 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 unser Listenfeld mit <select multiple> anstelle von <select size="3"> rendern. Der einzige Unterschied besteht darin, dass wir mehrere Optionen anstelle einer einzigen auswählen können. Die Gestaltung funktioniert auf genau die gleiche Weise.
Wir beginnen mit unserer Gestaltung, indem wir das <select>-Element mit einem appearance-Wert von base-select in die benutzerdefinierte Gestaltung übertragen:
select {
appearance: base-select;
}
Damit können wir nun unsere <select>- und <option>-Elemente nach Belieben gestalten.
Unsere grundlegenden Stile sehen wie folgt aus:
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-Pseudoelement, um das Häkchen für ausgewählte Optionen rechts statt links erscheinen zu lassen, und setzen ein benutzerdefiniertes Häkchen-Symbol mit der content-Eigenschaft.
option::checkmark {
order: 1;
margin-left: auto;
content: "☑️";
}
Schließlich setzen wir das font-weight für :checked-Optionen auf bold und eine benutzerdefinierte background-Farbe für die Zustände :hover und :focus, damit Sie immer wissen, welche Option Sie gehovt oder fokussiert haben.
option:checked {
font-weight: bold;
}
option:hover,
option:focus {
background: plum;
}
Dieses Beispiel wird wie folgt dargestellt:
Stilvariationen für Listenfelder
Da angepasste Listenfelder nur standardmäßige HTML-Elemente sind, können Sie sie nach Belieben gestalten. In diesem Abschnitt zeigen wir Ihnen einige Variationen des vorherigen Beispiels. Sie verwenden alle dasselbe oder ähnliches Markup; Wir haben ein bisschen zusätzliches CSS hinzugefügt, um das Aussehen und Gefühl deutlich zu ändern.
Erweitertes Listenfeld
In diesem Beispiel präsentieren wir das Listenfeld standardmäßig in der height einer einzelnen Option, verstecken den dadurch entstehenden overflow, und fügen eine transition hinzu, um die <select>-Höhe beim Zustandswechsel sanft zu animieren. Wir setzen auch einen interpolate-size-Wert von allow-keywords, um den Browser für das Animieren zwischen Längen und Schlüsselwörtern zu aktivieren.
select {
height: 44px;
overflow: hidden;
transition: 0.6s height;
interpolate-size: allow-keywords;
}
Wir ändern die height auf fit-content, wenn das <select> markiert oder fokussiert wird, damit es auf seine volle Höhe erweitert wird. Beachten Sie, dass beim Tabben in ein angepasstes Select das erste <option> den Fokus erhält, nicht das <select> selbst. Daher mussten wir select:has(option:focus) verwenden, um das <select> zu markieren, wenn eine <option> fokussiert ist, anstatt nur select:focus.
select:hover,
select:has(option:focus) {
height: fit-content;
}
Beispiel wird jetzt wie folgt dargestellt:
Horizontales Listenfeld
In diesem Beispiel präsentieren wir die Listenfeldoptionen horizontal anstatt vertikal.
Das HTML ist das gleiche wie bei den vorherigen Beispielen, außer dass wir einen zusätzlichen Wrapper-<div> hinzugefügt haben, um uns zu ermöglichen, eine width auf das <select> zu setzen und dann eine andere width auf den Wrapper, so dass alle <option>-Elemente in einer Zeile gehalten werden und gescrollt werden können, wenn das <select> zu schmal wird, um sie alle aufzunehmen.
<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 enthaltene <p>-Element und seine width und margin so zu setzen, dass das Demo horizontal im Viewport zentriert und die meiste Breite einnimmt. Wir skalieren dann das <select>, damit 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; und wir setzen seine width, damit sie immer so breit ist wie die <option>-Elemente.
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 zusätzliche Polsterung, um sie horizontal zu verteilen, und einen relativen position-Wert, damit wir ihre Nachkommen relativ zu ihnen positionieren können.
option {
padding: 10px 30px;
position: relative;
}
Zum Schluss positionieren wir die Optionskontrollkästchen absolut und geben ihnen ein benutzerdefiniertes Aussehen.
option::checkmark {
position: absolute;
top: -2px;
left: 2px;
font-size: 1.5rem;
color: red;
text-shadow: 1px 1px 1px black;
}
Unsere zweite Variation wird wie folgt dargestellt:
Ein komplexeres Listenfeld
In diesem Abschnitt gehen wir durch ein komplexeres Beispiel, das ein Kontakt-Auswahl-Listenfeld mit integriertem Filterfeld und einem Link zum Zugriff auf einen (fiktiven) Kontaktbearbeitungsmodus bereitstellt.
HTML
Im Markup fügen wir ein <form> ein, das eine Überschrift und ein Wrapper-<div> enthält. Im Wrapper fügen wir drei weitere <div>-Elemente ein, die jeweils ein Text<input>, das unser Filterfeld darstellt, ein Listenfeld-<select>, und einen Link enthalten. Das <select> wird durch JavaScript mit <option>-Elementen gefüllt, die unsere Kontaktoptionen 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 wie zuvor in benutzerdefinierte Stile übertragen:
select {
appearance: base-select;
}
Der Großteil des Stylings ist ziemlich rudimentär, aber wir gehen es durch und heben dabei alles Wesentliche hervor. Zuerst stylen wir das .wrapper-<div>, dem wir eine feste width geben, die die horizontale Größe des gesamten Steuerelements steuert.
.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> mit dem Link. Am bemerkenswertesten ist, dass wir dem <select> eine feste height und einen overflow-y-Wert von scroll geben, damit die enthaltenen <option>-Elemente darin scrollen.
.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 gestalten unsere <option>-Elemente in ähnlicher Weise wie in früheren Beispielen, indem wir ihnen ein Streifenmuster sowie klare :hover- und :focus-Stile geben:
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 besteht darin, den Standard-Fokusbereich für die <input>-, <option>- und <a>-Elemente zu entfernen. Wir haben in den vorherigen Codeblöcken bereits eine alternative Gestaltung für die <option>-Elemente 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 #999;
background: #eef;
}
.edit a {
color: #333;
}
a:hover,
a:focus {
outline: 2px dotted #666;
}
Schließlich bieten wir benutzerdefiniertes Styling für die Kontrollkästchen der ausgewählten Optionen über das ::checkmark-Pseudoelement:
option::checkmark {
order: 1;
margin-left: auto;
content: "☑️";
}
JavaScript
Die letzte Ergänzung, die unser Beispiel benötigt, ist ein wenig JavaScript, um die Optionserstellungs- und Filterfunktionalität zu ermöglichen.
Auf einer echten Website ziehen Sie wahrscheinlich eine aktuelle Kontaktliste von einem Server ab, aber in diesem Fall haben wir die Daten in einem statischen contacts-Objekt bereitgestellt (wir haben die meisten Kontakte der Kürze halber ausgeblendet). Für jeden Kontakt speichern wir einen Namen und ein boolean, das angibt, ob sie im <select>-Element ausgewählt waren.
const contacts = [
{ name: "Aisha Khan", selected: false },
...
];
Wir beginnen damit, Verweise auf unsere .filter-<input>- und <select>-Elemente abzurufen:
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 als Kind des <select> in das DOM eingefü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);
});
}
Nun definieren wir eine weitere Funktion, filterOptions(), die einen Filterstring und ein Array von Objekten als Parameter nimmt. Wir überprüfen, ob der String gleich dem leeren String oder einem oder mehreren Leerzeichen ist, indem wir den Rückgabewert seiner trim()-Methode mit "" vergleichen. Wenn dies true zurückgibt, führen wir die Funktion populateOptions() aus, übergeben ihr das vollständige Array, sodass 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() hat, dann übergeben wir das gefilterte Array an die populateOptions()-Funktion, damit das <select> mit einem gefilterten Satz <option>-Elemente 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 konvertieren sowohl den Objekt-name als auch den filter-String mit toLowerCase() in Kleinbuchstaben, sodass die Filterübereinstimmung nicht zwischen Groß- und Kleinschreibung unterscheidet.
Als Nächstes fügen wir dem .filter-<input>-Element einen input-Ereignislistener hinzu, sodass beim Bearbeiten seines Werts die filterOptions()-Funktion ausgeführt 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);
});
Im nächsten Codeabschnitt wird dem <select>-Element ein change-Ereignislistener hinzugefügt, 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 aktuell 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 deren ausgewählten Status umfasst. Wenn wir dies nicht tun würden, würden wir bei jedem Filterwechsel unsere ausgewählten Optionen verlieren.
Es gibt keine Möglichkeit, genau zu erkennen, welches <option> sich jedes Mal geändert hat, wenn eins umgeschaltet wird, also haben wir das Problem so gelöst:
- Holen Sie ein Array aller derzeit angezeigten
<option>-Werte, indem Sie ein Array aus derselect.options-Sammlung mitArray.fromerstellen, und dann das Array mit seinermap()-Methode so zuzuordnen, dass jedes<option>-Element im Array durch seinen Wert ersetzt wird. - Holen Sie ein Array aller aktuell ausgewählten
<option>-Werte mit der gleichen Methodik, aber dieses Mal erstellen wir das Eingabearray aus derselect.selectedOptions-Sammlung. - Für jedes Kontaktobjekt im
contacts-Array prüfen Sie, ob der Wert dername-Eigenschaft des Kontakts imallCurrentValues-Array enthalten ist, indem dieincludes()-Methode verwendet wird. Wenn nicht, ignorieren Sie es, damit wir nicht den ausgewählten Status der Kontakte umschalten, die überhaupt nicht angezeigt werden. Wenn 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 Objekt-Eigenschaft 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);
}
});
});
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.
populateOptions(contacts);
Ergebnis
Das Beispiel rendert wie folgt:
Als Nächstes
Im nächsten Artikel dieses Moduls werden wir die verschiedenen UI-Pseudoklassen untersuchen, die wir in modernen Browsern zur Gestaltung von Formularen in verschiedenen Zuständen verwenden können.