|
| 1 | +# Accessibility |
| 2 | + |
| 3 | +This component follows the [WAI-ARIA Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) to ensure a fully accessible experience for all users, including those using assistive technologies like screen readers. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +The component implements comprehensive accessibility features including: |
| 8 | + |
| 9 | +- **ARIA attributes** for proper semantic markup |
| 10 | +- **Keyboard navigation** for complete keyboard-only operation |
| 11 | +- **Focus management** for intuitive interaction flow |
| 12 | +- **Screen reader support** with descriptive labels and states |
| 13 | + |
| 14 | +## ARIA Attributes |
| 15 | + |
| 16 | +### Combobox Role |
| 17 | + |
| 18 | +The main container uses the `combobox` role to identify itself as an interactive input control: |
| 19 | + |
| 20 | +```html |
| 21 | +<div |
| 22 | + role="combobox" |
| 23 | + aria-expanded="true" |
| 24 | + aria-controls="vue-select-{uid}-listbox" |
| 25 | + aria-owns="vue-select-{uid}-listbox" |
| 26 | + aria-haspopup="true" |
| 27 | +> |
| 28 | +``` |
| 29 | + |
| 30 | +**Attributes:** |
| 31 | +- `aria-expanded`: Indicates whether the dropdown menu is open (`true`) or closed (`false`) |
| 32 | +- `aria-controls`: References the ID of the listbox element that this combobox controls |
| 33 | +- `aria-owns`: Indicates ownership of the listbox element |
| 34 | +- `aria-haspopup`: Signals that the combobox can trigger a popup menu |
| 35 | + |
| 36 | +### Listbox Role |
| 37 | + |
| 38 | +The dropdown menu uses the `listbox` role to identify itself as a list of selectable options: |
| 39 | + |
| 40 | +```html |
| 41 | +<div |
| 42 | + role="listbox" |
| 43 | + aria-multiselectable="true" |
| 44 | + aria-label="Select options" |
| 45 | +> |
| 46 | +``` |
| 47 | + |
| 48 | +**Attributes:** |
| 49 | +- `aria-multiselectable`: Set to `true` when `isMulti` prop is enabled, indicating multiple selections are allowed |
| 50 | +- `aria-label`: Provides an accessible name for the listbox (derived from the `aria.labelledby` prop) |
| 51 | + |
| 52 | +### Option Role |
| 53 | + |
| 54 | +Each menu option uses the `option` role with appropriate state attributes: |
| 55 | + |
| 56 | +```html |
| 57 | +<div |
| 58 | + role="option" |
| 59 | + aria-selected="true" |
| 60 | + aria-disabled="false" |
| 61 | +> |
| 62 | +``` |
| 63 | + |
| 64 | +**Attributes:** |
| 65 | +- `aria-selected`: Indicates whether the option is currently selected |
| 66 | +- `aria-disabled`: Indicates whether the option is disabled and cannot be selected |
| 67 | + |
| 68 | +### Search Input |
| 69 | + |
| 70 | +The search input includes autocomplete attributes for better screen reader support: |
| 71 | + |
| 72 | +```html |
| 73 | +<input |
| 74 | + aria-autocomplete="list" |
| 75 | + aria-labelledby="vue-select-{uid}-combobox" |
| 76 | + autocomplete="off" |
| 77 | + autocorrect="off" |
| 78 | + autocapitalize="none" |
| 79 | + spellcheck="false" |
| 80 | +> |
| 81 | +``` |
| 82 | + |
| 83 | +**Attributes:** |
| 84 | +- `aria-autocomplete="list"`: Indicates that autocomplete suggestions appear in a listbox |
| 85 | +- `aria-labelledby`: Associates the input with the combobox container for proper labeling |
| 86 | +- Standard HTML attributes to disable browser autocomplete features that might interfere with the component |
| 87 | + |
| 88 | +### Clear Button |
| 89 | + |
| 90 | +The clear button in multi-value tags includes an accessible label: |
| 91 | + |
| 92 | +```html |
| 93 | +<button |
| 94 | + type="button" |
| 95 | + aria-label="Remove {option-label}" |
| 96 | +> |
| 97 | +``` |
| 98 | + |
| 99 | +## Configurable ARIA Props |
| 100 | + |
| 101 | +You can customize ARIA attributes through the `aria` prop: |
| 102 | + |
| 103 | +```vue |
| 104 | +<VueSelect |
| 105 | + :aria="{ |
| 106 | + labelledby: 'my-custom-label', |
| 107 | + required: true |
| 108 | + }" |
| 109 | +/> |
| 110 | +``` |
| 111 | + |
| 112 | +**Available options:** |
| 113 | +- `labelledby`: ID of an element that labels the select component |
| 114 | +- `required`: Indicates whether selecting an option is required |
| 115 | + |
| 116 | +**Example:** |
| 117 | + |
| 118 | +```vue |
| 119 | +<template> |
| 120 | + <div> |
| 121 | + <label id="country-label">Select your country</label> |
| 122 | + <VueSelect |
| 123 | + v-model="selectedCountry" |
| 124 | + :options="countries" |
| 125 | + :aria="{ labelledby: 'country-label', required: true }" |
| 126 | + /> |
| 127 | + </div> |
| 128 | +</template> |
| 129 | +``` |
| 130 | + |
| 131 | +## Keyboard Navigation |
| 132 | + |
| 133 | +The component provides full keyboard support following WAI-ARIA best practices. |
| 134 | + |
| 135 | +### When Menu is Closed |
| 136 | + |
| 137 | +| Key | Action | |
| 138 | +|-----|--------| |
| 139 | +| <kbd>Space</kbd> | Opens the dropdown menu | |
| 140 | +| <kbd>Enter</kbd> | Opens the dropdown menu | |
| 141 | +| <kbd>↓</kbd> (Arrow Down) | Opens the dropdown menu and focuses first option | |
| 142 | +| <kbd>↑</kbd> (Arrow Up) | Opens the dropdown menu and focuses first option | |
| 143 | +| <kbd>Tab</kbd> | Moves focus to next focusable element | |
| 144 | + |
| 145 | +### When Menu is Open |
| 146 | + |
| 147 | +| Key | Action | |
| 148 | +|-----|--------| |
| 149 | +| <kbd>↓</kbd> (Arrow Down) | Moves focus to next non-disabled option (wraps to first option when reaching the end) | |
| 150 | +| <kbd>↑</kbd> (Arrow Up) | Moves focus to previous non-disabled option (wraps to last option when reaching the start) | |
| 151 | +| <kbd>Enter</kbd> | Selects the focused option and closes menu (if `closeOnSelect` is true) | |
| 152 | +| <kbd>Space</kbd> | Selects the focused option when search input is empty | |
| 153 | +| <kbd>Escape</kbd> | Closes the menu without selecting | |
| 154 | +| <kbd>Tab</kbd> | Closes the menu and moves focus to next element | |
| 155 | +| <kbd>PageDown</kbd> | Jumps focus to the last non-disabled option | |
| 156 | +| <kbd>PageUp</kbd> | Jumps focus to the first non-disabled option | |
| 157 | +| <kbd>Backspace</kbd> | When search is empty and there are selected values, removes the last selected option (multi-select) or the selected option (single-select) | |
| 158 | + |
| 159 | +### Search Behavior |
| 160 | + |
| 161 | +When `isSearchable` is enabled: |
| 162 | +- Typing automatically opens the menu if closed |
| 163 | +- Typing filters the available options in real-time |
| 164 | +- The focused option updates to the first matching result |
| 165 | + |
| 166 | +### Focus on Blur |
| 167 | + |
| 168 | +By default, when the component loses focus with the menu open, the focused option is automatically selected. You can disable this behavior with the `selectOnBlur` prop: |
| 169 | + |
| 170 | +```vue |
| 171 | +<VueSelect |
| 172 | + v-model="selected" |
| 173 | + :options="options" |
| 174 | + :select-on-blur="false" |
| 175 | +/> |
| 176 | +``` |
| 177 | + |
| 178 | +## Focus Management |
| 179 | + |
| 180 | +### Auto-focus Behavior |
| 181 | + |
| 182 | +When the menu opens, the component automatically focuses the first non-disabled option. You can disable this with the `shouldAutofocusOption` prop: |
| 183 | + |
| 184 | +```vue |
| 185 | +<VueSelect |
| 186 | + v-model="selected" |
| 187 | + :options="options" |
| 188 | + :should-autofocus-option="false" |
| 189 | +/> |
| 190 | +``` |
| 191 | + |
| 192 | +### Disabled Options |
| 193 | + |
| 194 | +Options with the `disabled` property are skipped during keyboard navigation and cannot be selected: |
| 195 | + |
| 196 | +```vue |
| 197 | +<VueSelect |
| 198 | + v-model="selected" |
| 199 | + :options="[ |
| 200 | + { label: 'Available', value: '1' }, |
| 201 | + { label: 'Unavailable', value: '2', disabled: true } |
| 202 | + ]" |
| 203 | +/> |
| 204 | +``` |
| 205 | + |
| 206 | +### Visual Focus Indicator |
| 207 | + |
| 208 | +Focused options receive the `.focused` CSS class, which applies visual styling (default: light blue background). You can customize this through CSS variables: |
| 209 | + |
| 210 | +```css |
| 211 | +:root { |
| 212 | + --vs-option-focused-background-color: #dbeafe; |
| 213 | + --vs-option-focused-text-color: #18181b; |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +### Viewport Scrolling |
| 218 | + |
| 219 | +When navigating with keyboard, the component automatically scrolls focused options into view if they're outside the menu's visible area. This ensures users never lose track of the focused option. |
| 220 | + |
| 221 | +## Screen Reader Support |
| 222 | + |
| 223 | +### Announced Changes |
| 224 | + |
| 225 | +Screen readers announce: |
| 226 | +- When the menu opens/closes via `aria-expanded` |
| 227 | +- The current selection via `aria-label` on the combobox |
| 228 | +- The focused option as users navigate with keyboard |
| 229 | +- Whether options are selected via `aria-selected` |
| 230 | +- Whether options are disabled via `aria-disabled` |
| 231 | +- The number of available options (via the listbox) |
| 232 | + |
| 233 | +### Value Container Label |
| 234 | + |
| 235 | +The combobox's `aria-label` dynamically updates to reflect the current selection: |
| 236 | + |
| 237 | +```html |
| 238 | +<!-- Single select: "Option Label" --> |
| 239 | +<div aria-label="Apple"> |
| 240 | + |
| 241 | +<!-- Multi-select: "Option 1, Option 2, Option 3" --> |
| 242 | +<div aria-label="Apple, Banana, Cherry"> |
| 243 | +``` |
| 244 | + |
| 245 | +### Non-searchable Mode |
| 246 | + |
| 247 | +When `isSearchable` is `false`, the search input receives additional attributes to hide it from screen readers: |
| 248 | + |
| 249 | +```html |
| 250 | +<input |
| 251 | + readonly |
| 252 | + tabindex="-1" |
| 253 | + aria-hidden="true" |
| 254 | +> |
| 255 | +``` |
| 256 | + |
| 257 | +This prevents screen readers from announcing the input, as it serves no purpose when search is disabled. |
| 258 | + |
| 259 | +## Input Attributes |
| 260 | + |
| 261 | +You can pass additional HTML attributes to the search input via the `inputAttrs` prop for enhanced accessibility: |
| 262 | + |
| 263 | +```vue |
| 264 | +<VueSelect |
| 265 | + v-model="selected" |
| 266 | + :options="options" |
| 267 | + :input-attrs="{ |
| 268 | + 'aria-describedby': 'helper-text', |
| 269 | + 'aria-invalid': hasError, |
| 270 | + 'aria-errormessage': hasError ? 'error-message' : undefined |
| 271 | + }" |
| 272 | +/> |
| 273 | +``` |
| 274 | + |
| 275 | +See the [Input Attributes guide](/guides/input-attributes) for more details. |
| 276 | + |
| 277 | +## Unique IDs |
| 278 | + |
| 279 | +Each select instance automatically generates a unique ID (via the `uid` prop) to ensure proper ARIA relationships between elements: |
| 280 | + |
| 281 | +```html |
| 282 | +<!-- Combobox --> |
| 283 | +<div id="vue-select-1-combobox" role="combobox" aria-controls="vue-select-1-listbox"> |
| 284 | + |
| 285 | +<!-- Listbox --> |
| 286 | +<div id="vue-select-1-listbox" role="listbox"> |
| 287 | +``` |
| 288 | + |
| 289 | +You can provide a custom UID if needed: |
| 290 | + |
| 291 | +```vue |
| 292 | +<VueSelect |
| 293 | + v-model="selected" |
| 294 | + :options="options" |
| 295 | + uid="custom-select-id" |
| 296 | +/> |
| 297 | +``` |
| 298 | + |
| 299 | +## Resources |
| 300 | + |
| 301 | +- [WAI-ARIA Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) |
| 302 | +- [WAI-ARIA Listbox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) |
| 303 | +- [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/) |
0 commit comments