Select Element

How to write an accessible native select element in Ember apps.

Introduction

As a general rule, the <select> element should be used (instead of radio buttons) if any of the following cases are true:

  1. there are more than 5-7 choices

  2. there are a large number of familiar options available (there is no need to be able to compare the options)

  3. the default choice is the recommended choice

It is a common misconception that the select element is not enough for web development. While there are some constraints on styling, the select element serves a specific purpose and should be used in those cases. It is ill-advised to disregard the select component entirely simply because it does not support more complex use cases out of the box. Instead, additional components should be created for complex functionality.

Part One: Considering Markup

The select element markup is rather straight-forward. All of the options are grouped together in one list or option groups are indicated.

<form>
<label for="select-color">Select your preferred color:</label>
<select name="colorPrefs" id="select-color">
<option value="red">Red</option>
<option value="orange">Orange</option>
<option value="yellow">Yellow</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="indigo">Indigo</option>
<option value="violet">Violet</option>
</select>
</form>

Benefits provided by the semantic html element:

  • if a user clicks on the label, the focus will automatically go to the select element

  • keyboard navigation is built in:

    • the SPACEBAR key will toggle the dropdown open/closed

    • the arrow keys will navigate up and down the list

    • the ESC key will close the dropdown

  • assistive technology (like screen readers) will already understand what this code is meant to do

If grouping is desired, the <optgroup> markup can be used:

<form>
<label for="select-activity">Select your preferred activity:</label>
<select name="activityPrefs" id="select-activity">
<optgroup label="Indoor">
<option value="indoor-sewing">Sewing</option>
<option value="indoor-painting">Painting</option>
<option value="indoor-baking">Baking</option>
</optgroup>
<optgroup label="Outdoor">
<option value="outdoor-climbing">Climbing</option>
<option value="outdoor-hiking">Hiking</option>
<option value="outdoor-horseback">Horseback Riding</option>
</optgroup>
</select>
</form>

While multiple selections can be allowed through the use of the <select multiple> attribute — most browsers will show a scrolling list box instead of a single line dropdown. It should be noted that use of the multiple attribute is not generally advised as it can be a confusing interface for users.

But what about when you need something more complex? As a general rule, it is far better to simplify the UX so that a native select can be used. However, there are considerations for when a custom select component is required.

An important part of successfully creating a custom select component is considering the accessibility aspect- how will assistive technology(AT) interact with your custom component, and are you providing the correct amount of information so that AT knows how your select was intended?

At the very least, developers should understand how a native select component is announced, that way steps can be taken to ensure that the custom version delivers the same value.

Using VoiceOver with Safari for Mac

  1. on focus, it says the name of the selected option (if it's not blank) then the element's label.

  2. on navigating the option list, each option's name should be announced

  3. on selected, using VO + SPACE, it should announce "press [option name]."

  4. if option is selected using the ENTER key instead, it does not say "press [option name]."

  5. regardless of how the selection is made, focus should return to the <select> element and it again says the name of the selected option and the element's label.

Using NVDA with Firefox (Windows)

  1. on focus, it announces these four things: label(name) + “combobox” + currently selected option + “collapsed”

  2. on ARROW up or down (without expanding the dropdown), it says the name of the option that is brought into focus (but it does not announce the position of the option in the list)

  3. on SPACE it opens the dropdown and announces these five things: “combobox expanded” + currently selected option + “list” + currently selected option + position of option in the list

  4. while the dropdown is open, navigating through the options with the arrow keys will cause the selected option’s name to be announced as well as its position in the list (such as “option c, 3 of 3”)

  5. pressing the ENTER key will return focus to the element and NVDA will announce these four things: label(name) + “combobox” + option selected + “collapsed” (note: nothing happened when I used the NVDA + SPACE or just SPACE).

Part Two: Creating the Ember Component

Just want something that works instead of rolling your own component? Try the ember-select-light addon, recently updated for the Octane edition of Ember!

The component should be generated (via ember-cli):

ember generate component select-element -gc

This will create three files and put them in the correct location:

  • app/components/select-element.hbs

  • app/components/select-element.js

  • tests/integration/components/select-element-test.js

In app/components/select-element.hbs the component markup can be set up and the places where dynamic functionality is needed can be indicated.

So this markup:

<form>
<label for="favoriteCity">Select Your Favorite City</label>
<select id="favoriteCity" name="select-favoriteCity">
<option value="Austin">Austin</option>
<option value="Boston">Boston</option>
<option value="Chicago">Chicago</option>
</select>
</form>

Becomes this component template:

<form>
<label for={{this.selectId}}>{{this.selectLabelText}}</label>
{{#let (array 'Austin' 'Boston' 'Chicago') as |selectElementOptions|}}
<select id={{this.selectId}} name={{this.selectName}} {{on "change" this.setSelection}}>
{{#each selectElementOptions as |selectElementOption|}}
<option value={{selectElementOption}} selected={{eq this.selectElementOption selectElementOption}}>{{selectElementOption}}</option>
{{/each}}
</select>
{{/let}}
</form>

Note: The above component template code relies on the popular ember-truth-helpers addon for its equality check for the looped <option>'s selected attribute.

In app/components/select-element.js the select id will need to be generated so the label element can access it. While there are a few different ways to accomplish this, the existing guidFor function will serve nicely:

import Component from '@glimmer/component';
import { guidFor } from '@ember/object/internals';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class SelectElementComponent extends Component {
@tracked selectElementOption;
selectId = `select-${guidFor(this)}`;
selectLabelText = 'Select Your Favorite City';
selectName = 'cityPreference';
@action
setSelection(changeEvent) {
let value = changeEvent.target.value;
this.selectElementOption = value;
}
}

Then, the component can be used in the view or page template:

<SelectElement />

Important Note: Due to a super weird bug in Firefox, you'll need to make sure that there is no whitespace around the <option> element content, or else the selected option will not be available to users with assistive technology.

Considering Attributes

Any form input planning should include considerations for which attributes should be supported. At the bare minimum, required, disabled, and readonly should be considered. Using ...attributes can be one way to give your component the flexibility it needs without having to preplan every attribute.

Remember: when a form is submitted, information marked as readonly will be sent to the server upon submit, whereas information marked disabled will not.

Leveraging Ember Addons

The Ember addon community is rich with ready-to-use components. For a simple and easily accessible <select> element, check out ember-select-light and assign it a <label>.

References

Feedback is welcome! Visit the GitHub repository for this project to raise an issue.