Dit is één mogelijke implementatie, er zijn meerdere implementaties mogelijk.
Een combobox is een invoerveld met een bijbehorende popup. Gebruikers kiezen een waarde uit de popup. Deze popup kan een list, grid, tree of dialog zijn. In dit voorbeeld gebruik ik een lijst.
Gebruik een combobox als:
- De waarde uit een voorgedefinieerde set moet komen
- Je willekeurige waarden toestaat maar suggesties wilt bieden
Er zijn twee hoofdtypen combobox:
- Select-only: gebruikers kunnen alleen een optie kiezen uit de lijst
- Editable: gebruikers kunnen typen en krijgen gefilterde opties
Technische opbouw
- Gebruik een
<input>
-element metrole="combobox"
- Geef het invoerveld een naam via
<label>
- (indien van toepassing) Geef het invoerveld
autocomplete
met een geldige waarde - Geef de popup
role="listbox"
- Geef de popup een uniek ID
- Koppel het invoerveld en de popup via
aria-controls
met het ID van de bijbehorende popup - Voor een uitgevouwen popup:
- Geeft het invoerveld de waarde
aria-expanded="true"
- Geeft het invoerveld de waarde
- Voor een samengevouwen popup:
- Geeft het invoerveld de waarde
aria-expanded="false"
- Geeft het invoerveld de waarde
- Geef opties in de lijst de rol
role="option"
- Voor geselecteerde opties:
- Geef de optie de waarde
aria-selected="true"
- Geef de optie de waarde
- Voor niet geselecteerde opties:
- Geef de optie de waarde
aria-selected="false"
(of helemaal geenaria-selected
)
- Geef de optie de waarde
- Geef het invoerveld
aria-activedescendant
met het ID van de gefocuste optie - Geef het invoerveld
aria-autocomplete
met de juiste waarde:none
: suggesties veranderen niet tijdens typenlist
: suggesties filteren op basis van getypte tekstboth
: gefilterde suggesties en vult automatisch aan aanvulling in het invoerveld
Optionele verbeteringen
- Geef het invoerveld
aria-haspopup="listbox"
Voorbeeld combobox
HTML
<!-- Combobox code -->
<label for="land-combo">
Kies je land:
</label>
<div class="combobox-container">
<input type="text" id="land-combo" class="combobox" role="combobox" aria-expanded="false" aria-controls="land-list" aria-activedescendant="" aria-autocomplete="list" autocomplete="country" placeholder="Type om te zoeken...">
<ul class="combobox-list" id="land-list" role="listbox">
<li class="combobox-option" role="option" aria-selected="false">
Nederland
</li>
<li class="combobox-option" role="option" aria-selected="false">
België
</li>
<li class="combobox-option" role="option" aria-selected="false">
Duitsland
</li>
<li class="combobox-option" role="option" aria-selected="false">
Frankrijk
</li>
</ul>
</div>
CSS
/* Combobox stijlen */
.combobox-container {
position: relative;
width: 300px;
}
.combobox {
width: 100%;
padding: 8px 12px;
border: 1px solid;
}
.combobox-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin: 0;
padding: 0;
background: white;
border: 1px solid;
border-top: none;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.combobox-list.show {
display: block;
}
.combobox-option {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid;
}
.combobox-option:hover,
.combobox-option.focused {
background: black;
color: white;
}
.combobox-option[aria-selected="true"] {
background: grey;
color: white;
}
JavaScript
// Combobox functionaliteit
class Combobox {
constructor(container) {
this.container = typeof container === 'string' ?
document.querySelector(container) : container;
if (!this.container) return;
this.input = this.container.querySelector('.combobox');
this.listbox = this.container.querySelector('.combobox-list');
this.options = Array.from(this.container.querySelectorAll('.combobox-option'));
this.focusIndex = -1;
this.selectedIndex = -1;
this.uniqueId = Date.now().toString(36);
// Stop als essentiële elementen ontbreken
if (!this.input || !this.listbox || this.options.length === 0) return;
this.setupAccessibility();
this.bindEvents();
}
setupAccessibility() {
// Zet aria-attributen voor het invoerveld
this.input.setAttribute('aria-expanded', 'false');
this.input.setAttribute('aria-haspopup', 'listbox');
this.input.setAttribute('aria-autocomplete', 'list');
this.input.setAttribute('aria-controls', this.listbox.id);
// Geef elke optie een uniek ID
this.options.forEach((option, index) => {
option.setAttribute('id', `opt-${this.uniqueId}-${index}`);
option.setAttribute('aria-selected', 'false');
});
}
bindEvents() {
// Invoerveld events
this.input.addEventListener('input', () => this.handleInput());
this.input.addEventListener('keydown', (e) => this.handleKeydown(e));
this.input.addEventListener('focus', () => this.openList());
this.input.addEventListener('blur', () => {
// Korte delay voor klik events op opties
setTimeout(() => this.closeList(), 150);
});
// Optie events
this.options.forEach((option, index) => {
option.addEventListener('click', () => this.selectOption(index));
option.addEventListener('mouseenter', () => this.setFocusOnOption(index));
});
}
handleInput() {
const searchText = this.input.value.toLowerCase();
let hasVisibleOptions = false;
// Filter opties op basis van input
this.options.forEach(option => {
const optionText = option.textContent.toLowerCase();
const matches = optionText.includes(searchText);
option.style.display = matches ? 'block' : 'none';
if (matches) hasVisibleOptions = true;
});
// Reset focus en toon lijst als er opties zijn
this.focusIndex = -1;
this.updateFocus();
if (hasVisibleOptions) {
this.openList();
} else {
this.closeList();
}
}
handleKeydown(e) {
const visibleOptions = this.getVisibleOptions();
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.focusIndex = Math.min(this.focusIndex + 1, visibleOptions.length - 1);
this.updateFocus();
this.openList();
break;
case 'ArrowUp':
e.preventDefault();
this.focusIndex = Math.max(this.focusIndex - 1, -1);
this.updateFocus();
break;
case 'Enter':
e.preventDefault();
if (this.focusIndex >= 0) {
const optionIndex = this.options.indexOf(visibleOptions[this.focusIndex]);
this.selectOption(optionIndex);
}
break;
case 'Escape':
this.closeList();
this.input.blur();
break;
case 'Tab':
this.closeList();
break;
}
}
setFocusOnOption(index) {
// Zet focus op specifieke optie (voor hover)
const visibleOptions = this.getVisibleOptions();
const visibleIndex = visibleOptions.indexOf(this.options[index]);
if (visibleIndex >= 0) {
this.focusIndex = visibleIndex;
this.updateFocus();
}
}
updateFocus() {
const visibleOptions = this.getVisibleOptions();
// Verwijder focus styling van alle opties
this.options.forEach(option => {
option.classList.remove('focused');
});
// Zet focus op actieve optie
if (this.focusIndex >= 0 && visibleOptions[this.focusIndex]) {
const focusedOption = visibleOptions[this.focusIndex];
focusedOption.classList.add('focused');
this.input.setAttribute('aria-activedescendant', focusedOption.id);
} else {
this.input.removeAttribute('aria-activedescendant');
}
}
selectOption(index) {
// Reset vorige selectie
this.options.forEach(option => {
option.setAttribute('aria-selected', 'false');
});
// Selecteer nieuwe optie
const selectedOption = this.options[index];
this.input.value = selectedOption.textContent.trim();
selectedOption.setAttribute('aria-selected', 'true');
this.selectedIndex = index;
this.closeList();
this.input.focus();
// Trigger change event voor formulier integratie
this.input.dispatchEvent(new Event('change', {
bubbles: true
}));
}
getVisibleOptions() {
return this.options.filter(option => option.style.display !== 'none');
}
openList() {
this.listbox.classList.add('show');
this.input.setAttribute('aria-expanded', 'true');
}
closeList() {
this.listbox.classList.remove('show');
this.input.setAttribute('aria-expanded', 'false');
this.focusIndex = -1;
this.updateFocus();
}
}
// Auto-initialisatie voor alle comboboxen
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.combobox-container').forEach(container => {
new Combobox(container);
});
});
Toetsenbordbediening
Invoerveld
- Escape:
- Bij een uitgevouwen popup: sluit de popup
- Pijltjestoetsen:
- Pijl omlaag: Verplaatst de focus naar het eerste element in de popup
- Tekens typen:
- Typt tekens
Popup
- Enter:
- Accepteert de gefocuste optie, sluit popup en keer terug naar de combobox
- Escape:
- Sluit de popup en keer terug naar combobox
- Pijltjestoetsen:
- Omlaag: Gaat naar de volgende optie in de popup
- Omhoog: Gaat naar de vorige optie in de popup
- Links/Rechts: Verplaatst de cursor in het invoerveld (als editable)
Optionele toetsen
- Home:
- Gaat naar de eerste optie in de popup
- End:
- Gaat naar de laatste optie in de popup
Focus management
- Focus blijft altijd op de combobox
- Houd de popup buiten de focus volgorde
Beoordeling
Component
- (4.1.2) Het component moet een
<input>
-element gebruiken - (4.1.2) Het invoerveld moet de rol
combobox
hebben (role="combobox"
) - (4.1.2) Het invoerveld moet een toegankelijke naam hebben
- (2.4.6) De toegankelijke naam moet beschrijven wat het doel van de combobox is
- (4.1.2) De popup moet de rol
listbox
hebben (role="listbox"
) - (
4.1.1) De listbox moet een uniek ID hebben - (1.3.1) Het invoerveld zou
aria-controls
moeten hebben met het ID van de popup - (4.1.2) Opties in de lijst moeten de rol
option
hebben (role="option"
) - (4.1.2) Geselecteerde opties moeten
aria-selected="true"
hebben - (4.1.2) Niet-geselecteerde opties moeten
aria-selected="false"
hebben (of geenaria-selected
) - (1.3.1) Het invoerveld moet
aria-activedescendant
hebben met het ID van de gefocuste optie - (4.1.2) Een uitgevouwen invoerveld moet
aria-expanded="true"
hebben - (4.1.2) Een samengevouwen invoerveld moet
aria-expanded="false"
hebben - (1.3.1) Het invoerveld moet
aria-autocomplete
hebben met:none
: suggesties veranderen niet tijdens typenlist
: suggesties filteren op getypte tekstboth
: filtering én automatische aanvulling