├── .gitignore ├── LICENSE ├── README.md ├── assets └── css │ ├── check-switch.css │ ├── on-off-switch.css │ └── toggle-switch.css ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARIA Switch Control 2 | Similar to a toggle button or checkbox, a switch control is meant to be used when its visual appearance most resembles an "on and off" "switch". 3 | 4 | The expected user experience of a switch control is for an immediate action to take place. For instance, toggling a light/dark theme for a website or application, where the change should instantly take effect. 5 | 6 | A checkbox, which is often found within a form, or in other UI where multiple elements can be checked, may not be understood to enact an immediate change to other elements or content in the document/screen. That's not to say additional information couldn't be presented to the user to indicate such functionality for a checkbox, but it's not a standard expectation. 7 | 8 | A toggle button and switch are a bit more similar in that they both have an expectation for an immediate change from user interaction. Their primary differences revolve around the manner in which they are supposed to communicate state to assistive technology users, as well as the visual design they each may be most associated with. 9 | 10 | A toggle button is typically announced as "pressed" or "selected" in its active state, where a switch should be announced as "on". 11 | 12 | 13 | ## How does it work? 14 | The baseline for this component requires one of the following markup patterns: 15 | 16 | ### Start as a ` 21 | ``` 22 | 23 | ### Start as a checkbox 24 | ```html 25 | 29 | ``` 30 | 31 | ### Start as a generic element 32 | For instance, such as a `
` or ``. 33 | ```html 34 | 37 | ``` 38 | 39 | ### Initial Attribute Breakdown 40 | `data-switch` is required for each markup pattern to be successfully transformed into a `role="switch"`. All setup and functionality is based around this attribute. The attribute can be set without a value, to default a switch to the "off" state. Setting the value to "on", e.g., `data-switch="on` will default the switch to the "on" state. Note: if using a checkbox as the base markup element, and the `checked` attribute will also set the switch to be "on" by default, even if `data-switch` has no value. 41 | 42 | You may notice that examples which do not have a checkbox base element have a default `disabled` or `hidden` attribute. This is due to the fact that these versions of the switch will not function without JavaScript. Rather than rendering a partially created that doesn't function, they can instead be disabled (for a ` 48 | 49 |

<span> base

50 | 53 | 54 |

<input type=checkbox> base

55 | 59 | 60 |

<div> base

61 | 64 | 65 |

<a> base

66 | 67 | Enable something else 68 | 69 | 70 | 71 |
72 |

73 | Pattern Details 74 |

75 | 76 |
77 | Pattern Markup (before script runs) 78 |
<button data-switch
 79 |   type="button"
 80 |   class="switch-toggle"
 81 |   data-switch-labels
 82 |   disabled
 83 | >
 84 |   Dark mode
 85 | </button>
 86 | 
 87 | <span data-switch class="switch-toggle" hidden>
 88 |   Dark mode (no visible state labels)
 89 | </span>
 90 | 
 91 | <label class="check-switch">
 92 |   <input type="checkbox" data-switch data-keep-disabled disabled>
 93 |   Enable dark mode
 94 | </label>
 95 | 
 96 | <div class="on-off-switch" data-switch id="enabler" hidden>
 97 |   Enable the next switch
 98 | </div>
 99 | 
100 | <a id="target"
101 |   href="#"
102 |   data-keep-disabled
103 |   aria-disabled="true"
104 |   class="on-off-switch"
105 |   data-switch
106 | >
107 |   Enable something else
108 | </a>
109 |
110 | 111 |
112 | Pattern Markup (after script runs) 113 |
<button data-switch 
114 |   type="button"
115 |   class="switch-toggle" 
116 |   data-switch-labels 
117 |   role="switch" 
118 |   aria-checked="false"
119 | >
120 |   Dark mode
121 |   <span aria-hidden="true" class="show-labels"></span>
122 | </button>
123 | 
124 | 
125 | <span data-switch 
126 |   class="switch-toggle" 
127 |   role="switch" 
128 |   tabindex="0" 
129 |   aria-checked="false"
130 | >
131 |   Dark mode (no visible state labels)
132 |   <span aria-hidden="true"></span>
133 | </span>
134 | 
135 | 
136 | <label class="check-switch">
137 |   <input type="checkbox" 
138 |     data-switch 
139 |     data-keep-disabled 
140 |     disabled 
141 |     role="switch"
142 |   >
143 |   Enable dark mode
144 |   <span aria-hidden="true"></span>
145 | </label>
146 | 
147 | 
148 | <div class="on-off-switch" 
149 |   data-switch 
150 |   id="enabler" 
151 |   role="switch"
152 |   tabindex="0" 
153 |   aria-checked="false"
154 | >
155 |   Enable the next switch
156 |   <span aria-hidden="true"></span>
157 | </div>
158 | 
159 | 
160 | <a id="target" 
161 |   data-keep-disabled 
162 |   aria-disabled="true" 
163 |   class="on-off-switch" 
164 |   data-switch 
165 |   role="switch" 
166 |   tabindex="-1" 
167 |   aria-checked="false"
168 | >
169 |   Enable something else
170 |   <span aria-hidden="true"></span>
171 | </a>
172 |
173 | 174 |

175 | A switch is meant to function similarly to a checkbox or toggle button, but assistive technologies are meant to announce it in a manner that is consistent with its on-screen appearance as an on/off sort of UI component. 176 |

177 | 178 |

179 | This particular pattern uses a <button> as the base element that is then modified into a role="switch" with JavaScript. When a <button> is modified to be exposed as a switch, the aria-checked attribute needs to be added, and managed, to appropriately convey the current state to assistive technologies (AT). 180 |

181 | 182 |

183 | For more information about switches and how AT, such as screen readers, interpret them, please review the checkbox switch pattern. 184 |

185 | 186 |

187 | For more information on the JavaScript and markup used, please see the 188 | ReadMe on the GitHub repo for this pattern. 189 |

190 | 191 |

Usage note

192 |

193 | Presently there are 194 | inconsistencies with screen readers and ARIA switches. 195 | 196 | Please review them to understand how the role is treated depending on screen reader and browser pairings, and if there are any gaps you may need to consider when considering the use of this role. 197 |

198 |
199 | 200 |

201 | Continue reading 202 |

203 |

204 | For more information about switches, please read the 205 | ARIA specification for the switch role. 206 | Inclusive Components Toggle Buttons, by Heydon Pickering. 207 |

208 |
209 | 210 | 211 | 212 | 213 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // add utilities 2 | const util = { 3 | generateID: function ( base ) { 4 | return base + Math.floor(Math.random() * 999); 5 | } 6 | }; 7 | 8 | 9 | (function ( w, doc, undefined ) { 10 | /** 11 | * A11Y ARIA Switch 12 | * 13 | * Author: Scott O'Hara 14 | * Version: 2.0.1 15 | * License: https://github.com/scottaohara/aria-switch-control/blob/main/LICENSE 16 | */ 17 | let ARIAswitchOptions = { 18 | baseID: 'aria_switch', 19 | defaultStateSelector: 'data-switch', 20 | showLabels: 'data-switch-labels', 21 | dataKeepDisabled: 'data-keep-disabled' 22 | }; 23 | 24 | 25 | const ARIAswitch = function ( inst, options ) { 26 | const _options = Object.assign(ARIAswitchOptions, options); 27 | const el = inst; 28 | let elID; 29 | let keepDisabledState; 30 | let initState; 31 | let isCheckbox; 32 | 33 | 34 | /** 35 | * Initialize the switch instance. 36 | * Create unique IDs and generate 37 | * a markup pattern if necessary. 38 | */ 39 | const init = function () { 40 | elID = el.id || util.generateID(_options.baseID); 41 | 42 | keepDisabledState = el.hasAttribute(_options.dataKeepDisabled); 43 | initState = el.getAttribute(_options.defaultStateSelector); 44 | 45 | setupBaseWidget() 46 | setupCheckedState(); 47 | generateToggleUI(); 48 | addEvents(); 49 | }; 50 | 51 | 52 | /** 53 | * Setup the baseline semantics of the widget, 54 | * ensuring that switches become/stay enabled/disabled, 55 | * and that only elements that need it get an appropriate 56 | * tabindex attribute. 57 | */ 58 | const setupBaseWidget = function () { 59 | el.setAttribute('role', 'switch'); 60 | 61 | // did this start off as a link? 62 | if ( el.href ) el.removeAttribute('href'); 63 | 64 | if ( !keepDisabledState ) { 65 | el.hidden = false; 66 | el.disabled = false; 67 | el.removeAttribute('aria-disabled'); 68 | } 69 | 70 | if ( el.hasAttribute('aria-disabled') ) { 71 | el.tabIndex = -1; 72 | } 73 | else { 74 | // if not a BUTTON or INPUT since those are focusable 75 | // by default 76 | if ( el.tagName !== 'BUTTON' && el.tagName !== 'INPUT' ) { 77 | el.tabIndex = 0; 78 | } 79 | } 80 | }; 81 | 82 | 83 | /** 84 | * Setup state depending on the type of starter element. 85 | */ 86 | const setupCheckedState = function () { 87 | if ( initState && (el || {}).type !== 'checkbox' ) { 88 | el.setAttribute('aria-checked', 'true'); 89 | } 90 | else if ( !initState && (el || {}).type !== 'checkbox' ) { 91 | el.setAttribute('aria-checked', 'false'); 92 | } 93 | 94 | if ( initState && (el || {}).type === 'checkbox' ) { 95 | el.checked = true; 96 | } 97 | }; 98 | 99 | 100 | /** 101 | * Add click and keypress events to elements 102 | */ 103 | const addEvents = function () { 104 | el.addEventListener('click', toggleState, false); 105 | 106 | if ( el.tagName !== 'BUTTON' ) { 107 | el.addEventListener('keypress', keyToggle, false); 108 | } 109 | }; 110 | 111 | 112 | /** 113 | * Generate the element to serve as the toggle 114 | * slider, or whatever version of the UI people 115 | * want to visually create. 116 | */ 117 | const generateToggleUI = function () { 118 | const ui = doc.createElement('span'); 119 | const hasLabels = el.hasAttribute(_options.showLabels); 120 | 121 | ui.setAttribute('aria-hidden', 'true'); 122 | 123 | if ( hasLabels ) ui.classList.add('show-labels'); 124 | 125 | // if a checkbox, append the UI as a sibling. 126 | // otherwise, as a child of the element. 127 | if ( (el || {}).type === 'checkbox' ) { 128 | el.parentNode.appendChild(ui); 129 | } 130 | else { 131 | el.appendChild(ui); 132 | } 133 | }; 134 | 135 | 136 | /** 137 | * Toggle between the on and off state of the switch. 138 | * Ignore switches that are baseline checkboxes. Checkboxes 139 | * can update their checked state natively and do not need 140 | * aria-checked. 141 | */ 142 | const toggleState = function ( e ) { 143 | if ( !el.hasAttribute('aria-disabled') ) { 144 | if ( (el || {}).type !== 'checkbox' ) { 145 | e.preventDefault(); 146 | el.setAttribute('aria-checked', el.getAttribute('aria-checked') === 'true' ? 'false' : 'true'); 147 | } 148 | } 149 | }; 150 | 151 | 152 | /** 153 | * Handle keyboard events for the switch. 154 | */ 155 | const keyToggle = function ( e ) { 156 | const keyCode = e.keyCode || e.which; 157 | 158 | switch ( keyCode ) { 159 | case 32: 160 | case 13: 161 | e.preventDefault(); 162 | toggleState(e); 163 | break; 164 | 165 | default: 166 | break; 167 | } 168 | 169 | /** 170 | * Checkboxes don't allow for Enter key to activate 171 | * by default. However, "switches" do have this 172 | * expectation, especially since they can be announced 173 | * as toggle buttons with some screen reader / browser pairings. 174 | */ 175 | if ( (el || {}).type === 'checkbox' ) { 176 | switch ( keyCode ) { 177 | case 13: 178 | e.preventDefault(); 179 | this.checked = this.checked == true ? false : true; 180 | break; 181 | 182 | default: 183 | break; 184 | } 185 | } 186 | } 187 | 188 | 189 | init.call( this ); 190 | return this; 191 | }; // ARIAswitch() 192 | 193 | w.ARIAswitch = ARIAswitch; 194 | })( window, document ); 195 | --------------------------------------------------------------------------------