Accessible toggle switch done the right way.
Personal preference: can be tabbed to, but does not gain focus on button/label click.
A Pen by IpsumLorem16 on CodePen.
Accessible toggle switch done the right way.
Personal preference: can be tabbed to, but does not gain focus on button/label click.
A Pen by IpsumLorem16 on CodePen.
<div class="toggle-container"> | |
<label for="toggleSwitch" class="toggle-switch"> | |
Toggle | |
</label> | |
<button role="switch" aria-checked="true" id="toggleSwitch" class="toggle-switch"> | |
<span aria-hidden="true">on</span> | |
<span class="knob"></span> | |
<span aria-hidden="true">off</span> | |
</button> | |
</div> |
document.querySelectorAll(".toggle-switch[role='switch']").forEach(switchEl => { | |
switchEl.addEventListener("click", handleToggleClick, false); | |
//prevent focus on switch when clicking on it | |
switchEl.addEventListener("mousedown", event => { | |
event.preventDefault(); | |
}); | |
}); | |
function handleToggleClick(event) { | |
let switchEl = event.target; | |
//if not disabled toggle attribute | |
if (!switchEl.hasAttribute("aria-readonly")) { | |
let currState = switchEl.getAttribute("aria-checked"); | |
let newState = currState === "true" ? false : true; | |
switchEl.setAttribute("aria-checked", newState); | |
} | |
} | |
//prevent focus to switch, on clicking label | |
document.querySelectorAll("label.toggle-switch").forEach(labelEl => { | |
labelEl.addEventListener("click", event => { | |
event.preventDefault(); //prevent focus | |
event.target.control.click(); //activate switch | |
}); | |
}); |
body { | |
background: #1d1d1d; | |
} | |
.toggle-container { | |
display: inline-block; | |
padding: 20px; | |
background-color: #ffffff1f; | |
} | |
button.toggle-switch { | |
position: relative; | |
width: 50px; | |
height: 26px; | |
padding: 1px 6px; | |
background-color: #d8d9db; | |
outline: none; | |
border: none; | |
color: white; | |
border-radius: 20px; | |
line-height: 20px; | |
cursor: pointer; | |
transition: all 0.3s; | |
/* prevent focus ring on mobile */ | |
-webkit-tap-highlight-color: transparent; | |
} | |
button.toggle-switch .knob { | |
position: absolute; | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background-color: white; | |
top: 3px; | |
left: 3px; | |
transition: all 0.3s; | |
} | |
button.toggle-switch span { | |
pointer-events: none; | |
} | |
label.toggle-switch { | |
margin-bottom: 4px; | |
margin-right: 4px; | |
font-family: Arial, sans-serif; | |
color: white; | |
user-select: none; | |
} | |
button.toggle-switch[aria-checked="true"] { | |
background-color: #4bd865; | |
} | |
button.toggle-switch[aria-checked="true"] .knob { | |
transform: translateX(24px); | |
} | |
button.toggle-switch[aria-checked="true"] :last-child { | |
opacity: 0; | |
transition: opacity 0.2s 0.1s; | |
} | |
button.toggle-switch[aria-checked="false"] :first-child { | |
opacity: 0; | |
transition: opacity 0.2s 0.1s; | |
} | |
/* disabled */ | |
button.toggle-switch[aria-readonly="true"]{ | |
cursor: not-allowed; | |
opacity:0.8; | |
filter: grayscale(50%); | |
} | |
button.toggle-switch:focus { | |
/* | |
for contrast against white background | |
box-shadow: 0px 0px 0px 3px #286bd0b0; */ | |
box-shadow: 0px 0px 0px 3px #fff; | |
} | |
/* hide outline on focus on firefox */ | |
button.toggle-switch::-moz-focus-inner { | |
border: 0; | |
outline: 0; | |
padding: 0; | |
} |