Accessibility

Simplified WCAG guidance

WCAG 2.2 – Levels A and AA only, with dev notes.

Accessible Combobox Input Selects

Also known as dropdown inputs (select)

A fork of Sarah Higley's indepth combobox examples, but with the Tab key action enabled.

Examples

HTML
HTML
<div class="combo-wrap">
	<!-- Basic HTML select element - recommended wherever possible -->
	<label for="html-select" class="combo-label">Native HTML Select Element</label>
	<div class="combo-select">
		<select id="html-select" class="combo-input html-select">
			<!-- <option>Apple</option> Injected from JS data -->
		</select>
	</div>
</div>

Note: id's must be unique

Styling a select element

HTML
HTML
<div class="combo-wrap">
	<label id="combo1-label" class="combo-label">Select Combobox Example <small>(select element equivalent)</small></label>
	<div class="combo js-select">
		<div
			aria-activedescendant="combo1-value"
			aria-autocomplete="none"
			aria-controls="listbox1"
			aria-expanded="false"
			aria-haspopup="listbox"
			aria-labelledby="combo1-label"
			id="combo1"
			class="combo-input"
			role="combobox"
			tabindex="0"
		>
			<span class="combo1-value" id="combo1-value">
				<!-- Selected option value injected by JavaScript-->
			</span>
		</div>
		<div class="combo-menu" role="listbox" id="listbox1">
			<!-- Options injected by JavaScript from data -->
		</div>
	</div>
</div>

Note: id's must be unique

Git repo: Select combobox

HTML
HTML
<div class="combo-wrap">
	<label for="combo2" class="combo-label">Editable Combobox Example</label>
	<div class="combo js-combobox">
		<input
			aria-activedescendant=""
			aria-autocomplete="none"
			aria-controls="listbox2"
			aria-expanded="false"
			aria-haspopup="listbox"
			id="combo2"
			class="combo-input"
			role="combobox"
			type="text"
		/>
		<div class="combo-menu" role="listbox" id="listbox2">
			<!-- Options injected by JavaScript from data -->
		</div>
	</div>
</div>

Note: id's must be unique

Git repo: Editable combobox

    HTML
    HTML
    <div class="combo-wrap">
    	<label id="combo3-label" class="combo-label">Multi-select Combobox Example</label>
    
    	<!-- Used as descriptive text for option buttons; if used within the button text itself, it ends up being read with the input name -->
    	<span id="combo3-remove" style="display: none">remove</span>
    
    	<ul class="selected-options" id="combo3-selected">
    		<!-- Selected options injected by JavaScript -->
    	</ul>
    	<div class="combo js-multiselect">
    		<input
    			aria-activedescendant=""
    			aria-autocomplete="none"
    			aria-controls="listbox3"
    			aria-expanded="false"
    			aria-haspopup="listbox"
    			aria-labelledby="combo3-label combo3-selected"
    			id="combo3"
    			class="combo-input"
    			role="combobox"
    			type="text"
    		/>
    		<div class="combo-menu" role="listbox" id="listbox3">
    			<!-- Options injected by JavaScript from data -->
    		</div>
    	</div>
    </div>
    

    Note: id's must be unique

    Git repo: Multi-select combobox

    The rest of the code

    JavaScript
    JS
    /*
    	* Helper constants and functions
    */
    
    // make it easier for ourselves by putting some values in objects
    // in TypeScript, these would be enums
    const Keys = {
    	Backspace: 'Backspace',
    	Clear: 'Clear',
    	Down: 'ArrowDown',
    	End: 'End',
    	Enter: 'Enter',
    	Escape: 'Escape',
    	Home: 'Home',
    	Left: 'ArrowLeft',
    	PageDown: 'PageDown',
    	PageUp: 'PageUp',
    	Right: 'ArrowRight',
    	Space: ' ',
    	Tab: 'Tab',
    	Up: 'ArrowUp'
    }
    
    const MenuActions = {
    	Close: 0,
    	CloseSelect: 1,
    	First: 2,
    	Last: 3,
    	Next: 4,
    	Open: 5,
    	Previous: 6,
    	Select: 7,
    	Space: 8,
    	Type: 9
    }
    
    // filter an array of options against an input string
    // returns an array of options that begin with the filter string, case-independent
    function filterOptions(options = [], filter, exclude = []) {
    	return options.filter((option) => {
    		const matches = option.toLowerCase().indexOf(filter.toLowerCase()) === 0;
    		return matches && exclude.indexOf(option) < 0;
    	});
    }
    
    // return an array of exact option name matches from a comma-separated string
    function findMatches(options, search) {
    	const names = search.split(',');
    	return names.map((name) => {
    		const match = options.filter((option) => name.trim().toLowerCase() === option.toLowerCase());
    		return match.length > 0 ? match[0] : null;
    	})
    	.filter((option) => option !== null);
    }
    
    // return combobox action from key press
    function getActionFromKey(event, menuOpen) {
    	const { key, altKey, ctrlKey, metaKey } = event;
    	// handle opening when closed
    	if (!menuOpen && (key === Keys.Down || key === Keys.Enter || key === Keys.Space)) {
    		return MenuActions.Open;
    	}
    
    	// handle keys when open
    	if (key === Keys.Down) {
    		return MenuActions.Next;
    	}
    	else if (key === Keys.Up) {
    		return MenuActions.Previous;
    	}
    	else if (key === Keys.Home) {
    		return MenuActions.First;
    	}
    	else if (key === Keys.End) {
    		return MenuActions.Last;
    	}
    	else if (key === Keys.Escape) {
    		return MenuActions.Close;
    	}
    	else if (key === Keys.Enter) {
    		return MenuActions.CloseSelect;
    	}
    	else if (key === Keys.Space) {
    		return MenuActions.Space;
    	}
    
    	// MJF added to replicate tab key usage when option selected
    	else if (key === Keys.Tab) {
    		if (menuOpen) return MenuActions.CloseSelect;
    	}
    
    	else if (key === Keys.Backspace || key === Keys.Clear || (key.length === 1 && !altKey && !ctrlKey && !metaKey)) {
    		return MenuActions.Type;
    	}
    }
    
    // get index of option that matches a string
    // if the filter is multiple iterations of the same letter (e.g "aaa"),
    // then return the nth match of the single letter
    function getIndexByLetter(options, filter) {
    	const firstMatch = filterOptions(options, filter)[0];
    	const allSameLetter = (array) => array.every((letter) => letter === array[0]);
    	console.log('testing string', filter);
    
    	if (firstMatch) {
    		return options.indexOf(firstMatch);
    	}
    	else if (allSameLetter(filter.split(''))) {
    		const matches = filterOptions(options, filter[0]);
    		const matchIndex = (filter.length - 1) % matches.length;
    		return options.indexOf(matches[matchIndex]);
    	}
    	else {
    		return -1;
    	}
    }
    
    // get updated option index
    function getUpdatedIndex(current, max, action) {
    	switch(action) {
    		case MenuActions.First:
    			return 0;
    		case MenuActions.Last:
    			return max;
    		case MenuActions.Previous:
    			return Math.max(0, current - 1);
    		case MenuActions.Next:
    			return Math.min(max, current + 1);
    		default:
    			return current;
    	}
    }
    
    // check if an element is currently scrollable
    function isScrollable(element) {
    	return element && element.clientHeight < element.scrollHeight;
    }
    
    // ensure given child element is within the parent's visible scroll area
    function maintainScrollVisibility(activeElement, scrollParent) {
    	const { offsetHeight, offsetTop } = activeElement;
    	const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent;
    
    	const isAbove = offsetTop < scrollTop;
    	const isBelow = (offsetTop + offsetHeight) > (scrollTop + parentOffsetHeight);
    
    	if (isAbove) {
    		scrollParent.scrollTo(0, offsetTop);
    	}
    	else if (isBelow) {
    		scrollParent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight);
    	}
    }
    
    /*
    	* Editable Combobox code
    */
    const Combobox = function(el, options) {
    	// element refs
    	this.el = el;
    	this.inputEl = el.querySelector('input');
    	this.listboxEl = el.querySelector('[role=listbox]');
    
    	// data
    	this.idBase = this.inputEl.id;
    	this.options = options;
    
    	// state
    	this.activeIndex = 0;
    	this.open = false;
    }
    
    Combobox.prototype.init = function() {
    	this.inputEl.value = options[0];
    
    	this.inputEl.addEventListener('input', this.onInput.bind(this));
    	this.inputEl.addEventListener('blur', this.onInputBlur.bind(this));
    	this.inputEl.addEventListener('click', () => this.updateMenuState(true));
    	this.inputEl.addEventListener('keydown', this.onInputKeyDown.bind(this));
    
    	this.options.map((option, index) => {
    		const optionEl = document.createElement('div');
    		optionEl.setAttribute('role', 'option');
    		optionEl.id = `${this.idBase}-${index}`;
    		optionEl.className = index === 0 ? 'combo-option option-current' : 'combo-option';
    		optionEl.setAttribute('aria-selected', `${index === 0}`);
    		optionEl.innerText = option;
    
    		optionEl.addEventListener('click', () => { this.onOptionClick(index); });
    		optionEl.addEventListener('mousedown', this.onOptionMouseDown.bind(this));
    
    		this.listboxEl.appendChild(optionEl);
    	});
    }
    
    Combobox.prototype.onInput = function() {
    	const curValue = this.inputEl.value;
    	const matches = filterOptions(this.options, curValue);
    
    	// set activeIndex to first matching option
    	// (or leave it alone, if the active option is already in the matching set)
    	const filterCurrentOption = matches.filter((option) => option === this.options[this.activeIndex]);
    	if (matches.length > 0 && !filterCurrentOption.length) {
    		this.onOptionChange(this.options.indexOf(matches[0]));
    	}
    
    	const menuState = this.options.length > 0;
    	if (this.open !== menuState) {
    		this.updateMenuState(menuState, false);
    	}
    }
    
    Combobox.prototype.onInputKeyDown = function(event) {
    	const max = this.options.length - 1;
    
    	const action = getActionFromKey(event, this.open);
    
    	switch(action) {
    		case MenuActions.Next:
    		case MenuActions.Last:
    		case MenuActions.First:
    		case MenuActions.Previous:
    			event.preventDefault();
    			return this.onOptionChange(getUpdatedIndex(this.activeIndex, max, action));
    		case MenuActions.CloseSelect:
    			event.preventDefault();
    			this.selectOption(this.activeIndex);
    			return this.updateMenuState(false);
    		case MenuActions.Close:
    			event.preventDefault();
    			return this.updateMenuState(false);
    		case MenuActions.Open:
    			return this.updateMenuState(true);
    	}
    }
    
    Combobox.prototype.onInputBlur = function() {
    	if (this.ignoreBlur) {
    		this.ignoreBlur = false;
    		return;
    	}
    
    	if (this.open) {
    		this.selectOption(this.activeIndex);
    		this.updateMenuState(false, false);
    	}
    }
    
    Combobox.prototype.onOptionChange = function(index) {
    	this.activeIndex = index;
    	this.inputEl.setAttribute('aria-activedescendant', `${this.idBase}-${index}`);
    
    	// update active style
    	const options = this.el.querySelectorAll('[role=option]');
    	[...options].forEach((optionEl) => {
    		optionEl.classList.remove('option-current');
    	});
    	options[index].classList.add('option-current');
    
    	if (this.open && isScrollable(this.listboxEl)) {
    		maintainScrollVisibility(options[index], this.listboxEl);
    	}
    }
    
    Combobox.prototype.onOptionClick = function(index) {
    	this.onOptionChange(index);
    	this.selectOption(index);
    	this.updateMenuState(false);
    }
    
    Combobox.prototype.onOptionMouseDown = function() {
    	this.ignoreBlur = true;
    }
    
    Combobox.prototype.selectOption = function(index) {
    	const selected = this.options[index];
    	this.inputEl.value = selected;
    	this.activeIndex = index;
    
    	// update aria-selected
    	const options = this.el.querySelectorAll('[role=option]');
    	[...options].forEach((optionEl) => {
    		optionEl.setAttribute('aria-selected', 'false');
    	});
    	options[index].setAttribute('aria-selected', 'true');
    }
    
    Combobox.prototype.updateMenuState = function(open, callFocus = true) {
    	this.open = open;
    
    	this.inputEl.setAttribute('aria-expanded', `${open}`);
    	open ? this.el.classList.add('open') : this.el.classList.remove('open');
    	callFocus && this.inputEl.focus();
    }
    
    
    // init combo
    const comboEl = document.querySelector('.js-combobox');
    const options = [
    	'Apple', 'Banana', 'Blueberry', 'Boysenberry',
    	'Cherry', 'Durian', 'Eggplant', 'Fig', 'Grape',
    	'Guava', 'Huckleberry'
    ];
    const comboComponent = new Combobox(comboEl, options);
    comboComponent.init();
    
    
    
    
    
    // init HTML select element (just creates options from a data set)
    const htmlSelect = document.getElementById('html-select');
    for (const optionValue of options) {
    	const option = document.createElement('option');
    	option.textContent = optionValue;
    	htmlSelect.appendChild(option)
    }
    
    
    
    /*
    	* Read-only select code
    */
    const Select = function(el, options) {
    	// element refs
    	this.el = el;
    	this.comboEl = el.querySelector('[role=combobox]');
    	this.valueEl = this.comboEl.querySelector('span');
    	this.listboxEl = el.querySelector('[role=listbox]');
    
    	// data
    	this.idBase = this.comboEl.id;
    	this.options = options;
    
    	// state
    	this.activeIndex = 0;
    	this.open = false;
    	this.searchString = '';
    	this.searchTimeout = null;
    }
    
    Select.prototype.init = function() {
    	this.valueEl.innerHTML = options[0];
    
    	this.comboEl.addEventListener('blur', this.onComboBlur.bind(this));
    	this.comboEl.addEventListener('click', () => this.updateMenuState(true));
    	this.comboEl.addEventListener('keydown', this.onComboKeyDown.bind(this));
    
    	this.options.map((option, index) => {
    		const optionEl = document.createElement('div');
    		optionEl.setAttribute('role', 'option');
    		optionEl.id = `${this.idBase}-${index}`;
    		optionEl.className = index === 0 ? 'combo-option option-current' : 'combo-option';
    		optionEl.setAttribute('aria-selected', `${index === 0}`);
    		optionEl.innerText = option;
    
    		optionEl.addEventListener('click', (event) => {
    			event.stopPropagation();
    			this.onOptionClick(index);
    		});
    		optionEl.addEventListener('mousedown', this.onOptionMouseDown.bind(this));
    
    		this.listboxEl.appendChild(optionEl);
    	});
    }
    
    Select.prototype.getSearchString = function(char) {
    	if (typeof this.searchTimeout === 'number') {
    		window.clearTimeout(this.searchTimeout);
    	}
    
    	this.searchTimeout = window.setTimeout(() => {
    		this.searchString = '';
    	}, 1000);
    
    	this.searchString += char;
    	return this.searchString;
    }
    
    Select.prototype.onComboKeyDown = function(event) {
    	const { key } = event;
    	const max = this.options.length - 1;
    
    	const action = getActionFromKey(event, this.open);
    
    	switch(action) {
    		case MenuActions.Next:
    		case MenuActions.Last:
    		case MenuActions.First:
    		case MenuActions.Previous:
    			event.preventDefault();
    			return this.onOptionChange(getUpdatedIndex(this.activeIndex, max, action));
    		case MenuActions.CloseSelect:
    		case MenuActions.Space:
    			event.preventDefault();
    			this.selectOption(this.activeIndex);
    		case MenuActions.Close:
    			event.preventDefault();
    			return this.updateMenuState(false);
    		case MenuActions.Type:
    			this.updateMenuState(true);
    			var searchString = this.getSearchString(key);
    			return this.onOptionChange(Math.max(0, getIndexByLetter(this.options, searchString)));
    		case MenuActions.Open:
    			event.preventDefault();
    			return this.updateMenuState(true);
    	}
    }
    
    Select.prototype.onComboBlur = function() {
    	if (this.ignoreBlur) {
    		this.ignoreBlur = false;
    		return;
    	}
    
    	if (this.open) {
    		this.selectOption(this.activeIndex);
    		this.updateMenuState(false, false);
    	}
    }
    
    Select.prototype.onOptionChange = function(index) {
    	this.activeIndex = index;
    	this.comboEl.setAttribute('aria-activedescendant', `${this.idBase}-${index}`);
    
    	// update active style
    	const options = this.el.querySelectorAll('[role=option]');
    	[...options].forEach((optionEl) => {
    		optionEl.classList.remove('option-current');
    	});
    	options[index].classList.add('option-current');
    
    	if (isScrollable(this.listboxEl)) {
    		maintainScrollVisibility(options[index], this.listboxEl);
    	}
    }
    
    Select.prototype.onOptionClick = function(index) {
    	this.onOptionChange(index);
    	this.selectOption(index);
    	this.updateMenuState(false);
    }
    
    Select.prototype.onOptionMouseDown = function() {
    	this.ignoreBlur = true;
    }
    
    Select.prototype.selectOption = function(index) {
    	const selected = this.options[index];
    	this.valueEl.innerHTML = selected;
    	this.activeIndex = index;
    
    	// update aria-selected
    	const options = this.el.querySelectorAll('[role=option]');
    	[...options].forEach((optionEl) => {
    		optionEl.setAttribute('aria-selected', 'false');
    	});
    	options[index].setAttribute('aria-selected', 'true');
    }
    
    Select.prototype.updateMenuState = function(open, callFocus = true) {
    	this.open = open;
    
    	this.comboEl.setAttribute('aria-expanded', `${open}`);
    	open ? this.el.classList.add('open') : this.el.classList.remove('open');
    	callFocus && this.comboEl.focus();
    
    	// update activedescendant
    	const activeID = open ? `${this.idBase}-${this.activeIndex}` : this.valueEl.id;
    	this.comboEl.setAttribute('aria-activedescendant', activeID);
    }
    
    // init select
    const selectEl = document.querySelector('.js-select');
    const selectComponent = new Select(selectEl, options);
    selectComponent.init();
    
    
    
    /*
    	* Multiselect code
    */
    const Multiselect = function(el, options) {
    	// element refs
    	this.el = el;
    	this.inputEl = el.querySelector('input');
    	this.listboxEl = el.querySelector('[role=listbox]');
    
    	this.idBase = this.inputEl.id;
    	this.selectedEl = document.getElementById(`${this.idBase}-selected`);
    
    	// data
    	this.options = options;
    
    	// state
    	this.activeIndex = 0;
    	this.open = false;
    }
    
    Multiselect.prototype.init = function() {
    	this.inputEl.addEventListener('input', this.onInput.bind(this));
    	this.inputEl.addEventListener('blur', this.onInputBlur.bind(this));
    	this.inputEl.addEventListener('click', () => this.updateMenuState(true));
    	this.inputEl.addEventListener('keydown', this.onInputKeyDown.bind(this));
    	this.listboxEl.addEventListener('blur', this.onInputBlur.bind(this));
    
    	this.options.map((option, index) => {
    		const optionEl = document.createElement('div');
    		optionEl.setAttribute('role', 'option');
    		optionEl.id = `${this.idBase}-${index}`;
    		optionEl.className = index === 0 ? 'combo-option option-current' : 'combo-option';
    		optionEl.setAttribute('aria-selected', 'false');
    		optionEl.innerText = option;
    
    		optionEl.addEventListener('click', () => { this.onOptionClick(index); });
    		optionEl.addEventListener('mousedown', this.onOptionMouseDown.bind(this));
    
    		this.listboxEl.appendChild(optionEl);
    	});
    }
    
    Multiselect.prototype.onInput = function() {
    	const curValue = this.inputEl.value;
    	const matches = filterOptions(this.options, curValue);
    
    	// set activeIndex to first matching option
    	// (or leave it alone, if the active option is already in the matching set)
    	const filterCurrentOption = matches.filter((option) => option === this.options[this.activeIndex]);
    	if (matches.length > 0 && !filterCurrentOption.length) {
    		this.onOptionChange(this.options.indexOf(matches[0]));
    	}
    
    	const menuState = this.options.length > 0;
    	if (this.open !== menuState) {
    		this.updateMenuState(menuState, false);
    	}
    }
    
    Multiselect.prototype.onInputKeyDown = function(event) {
    	const max = this.options.length - 1;
    
    	const action = getActionFromKey(event, this.open);
    
    	switch(action) {
    		case MenuActions.Next:
    		case MenuActions.Last:
    		case MenuActions.First:
    		case MenuActions.Previous:
    			event.preventDefault();
    			return this.onOptionChange(getUpdatedIndex(this.activeIndex, max, action));
    		case MenuActions.CloseSelect:
    			event.preventDefault();
    			return this.updateOption(this.activeIndex);
    			// return this.updateMenuState(false);
    		case MenuActions.Close:
    			event.preventDefault();
    			return this.updateMenuState(false);
    		case MenuActions.Open:
    			return this.updateMenuState(true);
    	}
    }
    
    Multiselect.prototype.onInputBlur = function() {
    	if (this.ignoreBlur) {
    		this.ignoreBlur = false;
    		return;
    	}
    
    	if (this.open) {
    		this.updateMenuState(false, false);
    	}
    }
    
    Multiselect.prototype.onOptionChange = function(index) {
    	this.activeIndex = index;
    	this.inputEl.setAttribute('aria-activedescendant', `${this.idBase}-${index}`);
    
    	// update active style
    	const options = this.el.querySelectorAll('[role=option]');
    	[...options].forEach((optionEl) => {
    		optionEl.classList.remove('option-current');
    	});
    	options[index].classList.add('option-current');
    
    	if (this.open && isScrollable(this.listboxEl)) {
    		maintainScrollVisibility(options[index], this.listboxEl);
    	}
    }
    
    Multiselect.prototype.onOptionClick = function(index) {
    	this.onOptionChange(index);
    	this.updateOption(index);
    	this.inputEl.focus();
    }
    
    Multiselect.prototype.onOptionMouseDown = function() {
    	this.ignoreBlur = true;
    }
    
    Multiselect.prototype.removeOption = function(index) {
    	const option = this.options[index];
    
    	// update aria-selected
    	const options = this.el.querySelectorAll('[role=option]');
    	options[index].setAttribute('aria-selected', 'false');
    	options[index].classList.remove('option-selected');
    
    	// remove button
    	const buttonEl = document.getElementById(`${this.idBase}-remove-${index}`);
    	this.selectedEl.removeChild(buttonEl.parentElement);
    }
    
    Multiselect.prototype.selectOption = function(index) {
    	const selected = this.options[index];
    	this.activeIndex = index;
    
    	// update aria-selected
    	const options = this.el.querySelectorAll('[role=option]');
    	options[index].setAttribute('aria-selected', 'true');
    	options[index].classList.add('option-selected');
    
    	// add remove option button
    	const buttonEl = document.createElement('button');
    	const listItem = document.createElement('li');
    	buttonEl.className = 'remove-option';
    	buttonEl.type = 'button';
    	buttonEl.id = `${this.idBase}-remove-${index}`;
    	buttonEl.setAttribute('aria-describedby', `${this.idBase}-remove`);
    	buttonEl.addEventListener('click', () => { this.removeOption(index); });
    	buttonEl.innerHTML = selected + ' ';
    
    	listItem.appendChild(buttonEl);
    	this.selectedEl.appendChild(listItem);
    }
    
    Multiselect.prototype.updateOption = function(index) {
    	const option = this.options[index];
    	const optionEl = this.el.querySelectorAll('[role=option]')[index];
    	const isSelected = optionEl.getAttribute('aria-selected') === 'true';
    
    	if (isSelected) {
    		this.removeOption(index);
    	} else {
    		this.selectOption(index);
    	}
    
    	this.inputEl.value = '';
    }
    
    Multiselect.prototype.updateMenuState = function(open, callFocus = true) {
    	this.open = open;
    
    	this.inputEl.setAttribute('aria-expanded', `${open}`);
    	open ? this.el.classList.add('open') : this.el.classList.remove('open');
    	callFocus && this.inputEl.focus();
    }
    
    // init multiselect
    const multiselectEl = document.querySelector('.js-multiselect');
    const multiselectComponent = new Multiselect(multiselectEl, options);
    multiselectComponent.init();			
    
    CSS
    CSS
    .combo-wrap {
    	/* Component scoped colour variables
    		- values from the global scope or default */
    	--_combo-border: var(--border, #ccc);
    	--_combo-background: var(--accent-bg, current);
    	--_combo-color: var(--text, currentColor);
    	--_combo-background-highlight: var(--bg, current);
    }
    
    /* HTML Select */
    .html-select {
    	appearance: none;
    }
    
    .combo {
    	display: block;
    	max-width: 400px;
    	position: relative;
    	margin: 0.25rem 0;
    }
    
    /* Fancy technique to use border as a dropdown icon
    		- use an icon instead! */
    .combo::after {
    	border: 2px solid var(--_combo-border-color);
    	border-width: 0 2px 2px 0;
    	content: "";
    	display: block;
    	height: 12px;
    	pointer-events: none;
    	position: absolute;
    	right: 16px;
    	top: 50%;
    	transform: translate(0, -65%) rotate(45deg);
    	width: 12px;
    }
    
    .combo-input {
    	background-color: var(--_combo-background);
    	border: 2px solid var(--_combo-border-color);
    	border-radius: 4px;
    	display: block;
    	padding: .25em .5em;
    	text-align: left;
    	width: 100%;
    	font: inherit;
    	letter-spacing: inherit;
    	word-spacing: inherit;
    }
    
    .open .combo-input {
    	border-radius: 4px 4px 0 0;
    }
    
    .combo-label {
    	display: block;
    	line-height: 1.3;
    }
    
    .combo-menu {
    	background-color: var(--_combo-background);
    	border: 1px solid var(--_combo-border-color);
    	border-radius: 0 0 4px 4px;
    	display: none;
    	max-height: 300px;
    	overflow-y: scroll;
    	left: 0;
    	position: absolute;
    	top: 100%;
    	width: 100%;
    	z-index: 100;
    	margin-top:4px;
    	box-shadow: 4px 4px 4px #000c;
    }
    
    .open .combo-menu {
    	display: block;
    }
    
    .combo-option {
    	margin: 1px 0 0;
    	padding: 10px 12px 12px;
    	position: relative;
    }
    .combo-option::before {
    	content: '';
    	position: absolute;
    	top: 0;
    	left: 0;
    	width: 100%;
    	height: 100%;
    	background-color: var(--_combo-background-highlight);
    	opacity: 0;
    	z-index: -1;
    }
    .combo-option.option-current::before,
    .combo-option:hover::before {
    	opacity: 1;
    }
    
    .combo-option.option-selected {
    	padding-right: 30px;
    	position: relative;
    }
    /* Fancy technique to use border as a tick icon
    		- use an icon instead! */
    .combo-option[aria-selected=true]::after {
    	border: 2px solid var(--_combo-color);
    	border-width: 0 2px 2px 0;
    	content: "";
    	height: 16px;
    	position: absolute;
    	right: 15px;
    	top: 50%;
    	transform: translate(0, -50%) rotate(45deg);
    	width: 8px;
    }
    
    /* multiselect list of selected options */
    .selected-options {
    	list-style-type: none;
    	margin: 0;
    	padding: 0;
    	display: flex;
    	flex-wrap: wrap;
    	gap: 8px;
    }
    
    ul.selected-options li + li {
    	margin:0 !important;
    }
    
    button.remove-option {
    	font-size: 0.75em;
    	font-weight: bold;
    	padding: 0.25em 1.75em 0.25em 0.25em;
    	position: relative;
    }
    
    /* Fancy technique to use border as a cross icon
    		- use an icon instead! */
    button.remove-option::before,
    button.remove-option::after {
    	border-right: 2px solid currentColor;
    	content: "";
    	height: 1em;
    	right: 0.75em;
    	position: absolute;
    	top: 50%;
    	width: 0;
    }
    button.remove-option::before {
    	transform: translate(0, -50%) rotate(45deg);
    }
    button.remove-option::after {
    	transform: translate(0, -50%) rotate(-45deg);
    }