DEV Community

Tiny Octopus
Tiny Octopus

Posted on • Updated on • Originally published at tiny-octopus.com

How to Build An Accessible Toggle Switch with Modern HTML & CSS (Without JavaScript)

When your designer hands you a slick UI featuring toggle switches, your priority is to make sure these switches are accessible. So, how do you go about building an accessible toggle switch without relying on JavaScript?

Building an Accessible Toggle Switch with HTML and CSS

JavaScript is a powerful tool, and we use it every day at Tiny Octopus. However, browser vendors have equipped us with the necessary features to build accessible components without it. By using HTML and CSS, you can create a toggle switch that’s both functional and accessible.

View the finished Accessible Toggle Switch: https://codepen.io/Tiny-Octopus/pen/xxoWLZP?editors=1100

Step 1: Start with an HTML Checkbox

At the very beginning, we'll start with a well-structured HTML checkbox. This uses an <input> element paired with a <label> element, ensuring they are correctly attributed and displaying a visible label.

<label for="toggle">
  <input type="checkbox" name="toggle" id="toggle" />
  This is the toggle label
</label>
Enter fullscreen mode Exit fullscreen mode

An unstyled HTML radio form element

To meet WCAG Success Criteria 1.4.1 (Use of Color), avoid conveying the checkbox status solely through color. Instead, we're going to add in two icons (sourced from Material UI) to indicate the state of the checkbox.

Place a <span> between the checkbox and the label to hold the check and cross icons. We'll then use a pseudo-element to create a toggle that covers one icon at a time, visually indicating whether the checkbox is checked or unchecked.

<label class="toggle" for="toggle">
  <input type="checkbox" name="toggle" id="toggle" class="toggle__input" />

  <span class="toggle__display" hidden>
    <svg aria-hidden="true" focusable="false" class="toggle__icon toggle__icon--checkmark" width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M6.08471 10.6237L2.29164 6.83059L1 8.11313L6.08471 13.1978L17 2.28255L15.7175 1L6.08471 10.6237Z" fill="currentcolor" stroke="currentcolor" />
    </svg>
    <svg aria-hidden="true" focusable="false" class="toggle__icon toggle__icon--cross" width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M11.167 0L6.5 4.667L1.833 0L0 1.833L4.667 6.5L0 11.167L1.833 13L6.5 8.333L11.167 13L13 11.167L8.333 6.5L13 1.833L11.167 0Z" fill="currentcolor" />
    </svg>
  </span>

  This is the toggle label
</label>
Enter fullscreen mode Exit fullscreen mode

Key HTML Considerations

  • Using the hidden Attribute: Apply the hidden attribute to the <span> containing the SVG icons. This ensures that if CSS fails to load, users still see a simple checkbox with a label. The hidden value will be overridden in CSS.
  • Aria Attributes: Set aria-hidden="true" on the SVGs, as they are decorative and should not be announced by screen readers.
  • Focusability: Add focusable="false" to the SVGs to prevent an issue in Internet Explorer where SVGs are focusable by default.

Step 2: Style the Toggle Switch with CSS

Now that the HTML is in place, it’s time to style our toggle switch to make it visually appealing and accessible.

/* 
  Define color variables using CSS custom properties for easy maintenance and theming.
  This allows for quick updates and supports light/dark mode switching.
*/
:root {
  --color-toggle-bg-default: #fbe4e2; 
  --color-toggle-bg-checked: #cce6d0; 
  --color-toggle-border: rgba(0, 0, 0, 0.2);
  --color-focus-ring: #4c9aff;
  --color-icon-checkmark: #006838;
  --color-icon-cross: #9e1b1b;
}

/* 
  Set up the basic structure of the toggle component. 
  Using flexbox to align items and create spacing between the elements.
*/
.toggle {
  display: flex;
  align-items: center;
  position: relative;
  cursor: pointer; 
  gap: 1rem;
  flex-wrap: wrap;
}

/* 
  Hide the actual checkbox input but keep it accessible.
  Position it absolutely to cover the toggle switch area, making the entire area clickable.
*/
.toggle__input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
}

/* 
  Styling the visual representation of the toggle switch.
  Using CSS variables for sizing and spacing, and applying a smooth transition for state changes.
*/
.toggle__display {
  --offset: 0.25em;
  --diameter: 1.8em;

  display: inline-flex;
  align-items: center;
  justify-content: space-around;
  box-sizing: content-box;
  width: calc(var(--diameter) * 2 + var(--offset) * 2);
  height: calc(var(--diameter) + var(--offset) * 2);
  border: 0.1em solid var(--color-toggle-border);
  position: relative;
  border-radius: 100vw;
  background-color: var(--color-toggle-bg-default);
  transition: background-color 250ms, transform 250ms;
}

/* 
  The knob inside the toggle switch, which moves when toggled.
  Positioned absolutely inside the switch and centered vertically.
*/
.toggle__display::before {
  content: "";
  z-index: 2;
  position: absolute;
  top: 50%;
  inset-inline-start: var(--offset);
  box-sizing: border-box;
  width: var(--diameter);
  height: var(--diameter);
  border: 0.1em solid var(--color-toggle-border);
  border-radius: 50%;
  background-color: white;
  transform: translateX(0) translateY(-50%);
  will-change: transform;
  transition: transform 250ms;
}

/* 
  Focus-visible is used to apply the focus ring only when navigating via keyboard.
  This prevents the focus ring from appearing during mouse clicks, enhancing the visual experience.
*/
.toggle__input:focus-visible + .toggle__display {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 4px;
}

/* 
  Change the background color when the toggle is checked (active state).
  This applies the green color to indicate the "on" state.
*/
.toggle__input:checked + .toggle__display {
  background-color: var(--color-toggle-bg-checked); 
}

/* 
  Move the knob to the right when the toggle is checked.
  This visually represents the "on" state.
*/
.toggle__input:checked + .toggle__display::before {
  transform: translateX(100%) translateY(-50%);
}

/* 
  Styling for the icons inside the toggle (e.g., checkmark and cross).
  Using inline-block for flexibility and inheriting colors for easy theming.
*/
.toggle__icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  color: inherit;
  fill: currentcolor;
  vertical-align: middle;
  overflow: hidden;
}

/* 
  Specific styling for the "cross" icon (used for "off" state).
  A slightly smaller size and dark red color.
*/
.toggle__icon--cross {
  color: var(--color-icon-cross);
  font-size: 85%;
}

/* 
  Specific styling for the "checkmark" icon (used for "on" state).
  A dark green color that contrasts well with the light green background.
*/
.toggle__icon--checkmark {
  color: var(--color-icon-checkmark);
}
Enter fullscreen mode Exit fullscreen mode

Animation showing toggle switch in action

Ensuring Accessibility in Your Toggle Switch Design

To create an accessible toggle switch, consider the following key areas:

Focus State

A clear focus state is crucial. Ensure that your toggle switch’s focus state is distinct and meets the required color contrast ratios. Opt for outline over box-shadow for better accessibility.

Clear Labeling

Your toggle switch must have a clear label, either visible or provided via an aria-label attribute. This helps screen reader users understand the function of the switch.

New Safari Switch Attribute

Safari recently introduced an exciting new feature: the input[switch] control, available in Safari 17.4+. This new control supports standardized pseudo-elements like ::thumb and ::track for styling form controls, moving away from the older ::-webkit prefixed elements.

These advancements in Safari not only make CSS more maintainable but also allow for creative, interactive toggle designs. For example, Apple’s Webkit blog features a Light/Dark mode toggle switch with embedded animations, showcasing the potential of these new features.

While these updates are promising, always consider the user experience. Toggle switches should be used in scenarios where a setting can be clearly understood as being either “on” or “off.”

Top comments (0)