HTML & CSS 3d ago 5 views 5 min read

How to build a pure CSS checkbox toggle switch

Create a functional toggle switch using only HTML and CSS without JavaScript. Learn the hidden input trick, the pseudo-element styling, and how to handle focus states for accessibility.

Riya K.
Updated 1d ago
Sponsored

Cloud VPS — scale in minutes

Instantly deploy SSD cloud VPS with guaranteed resources, snapshots and per-hour billing. Pay only for what you use.

Build a functional toggle switch using only HTML and CSS without any JavaScript. You will learn to hide the default checkbox, style the label as the track, and use the `:checked` pseudo-class to change colors when the user clicks. This method ensures smooth performance and full browser compatibility across all modern devices.

Prerequisites

  • A text editor like VS Code, Sublime Text, or Nano.
  • A modern web browser (Chrome, Firefox, Safari, or Edge) with CSS support.
  • Basic understanding of HTML structure and CSS selectors.
  • A local HTML file or a live web server to test the changes.

Step 1: Create the HTML structure

Start by creating a standard HTML file. You need a container div to hold the switch, a hidden checkbox input, and a label that acts as the clickable button. The label must have a `for` attribute matching the input's `id` to link them.

<div class="switch">
  <input type="checkbox" id="toggle" class="toggle">
  <label for="toggle" class="toggle-label"></label>
</div>

Save this code as `index.html` in your project folder. Open the file in your browser to see the default ugly checkbox before applying styles.

Step 2: Hide the default checkbox

Remove the visual appearance of the default browser checkbox while keeping it accessible to keyboard users. Use the `appearance: none` property or the `clip-path` method to hide the input element completely. This ensures the user only sees your custom design.

.toggle {
  display: none;
}

Alternatively, you can use the clip-path method if you need to maintain the input size for layout purposes, though `display: none` is cleaner for pure CSS toggles.

Step 3: Style the toggle track

Create the visual track that represents the on/off state. Use a `div` or `label` to act as the track background. Set a width that matches your desired switch size, usually around 40px to 60px. Apply a background color for the "off" state and a border radius to make it look rounded.

.toggle-label {
  position: relative;
  display: inline-block;
  width: 50px;
  height: 26px;
  background-color: #ccc;
  border-radius: 34px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.toggle-label::after {
  content: "";
  position: absolute;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  top: 1px;
  left: 1px;
  background-color: white;
  transition: left 0.3s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

The `::after` pseudo-element acts as the sliding circle. Position it inside the track using absolute positioning. The transition property creates the smooth sliding animation when the state changes.

Step 4: Style the "on" state

Change the track color and the circle position when the checkbox is checked. Use the `input:checked + label` selector to target the label only when the input is active. Move the circle to the right side of the track using the `left` property. Change the background color to indicate the active state.

input:checked + .toggle-label {
  background-color: #4CAF50; /* Green for on */
}

input:checked + .toggle-label::after {
  transform: translateX(24px);
}

Notice the use of `transform: translateX()` instead of changing the `left` property directly. Using `transform` is more performant because it doesn't trigger a full layout recalculation in the browser.

Step 5: Add text labels (Optional)

Add text inside the switch to show "ON" and "OFF". Place a span inside the label and position it absolutely. Use `opacity` to show or hide the text based on the state. This keeps the HTML semantic while adding visual information.

.toggle-label::before {
  content: "OFF";
  position: absolute;
  left: 5px;
  top: 50%;
  transform: translateY(-50%);
  color: white;
  font-size: 10px;
  font-weight: bold;
  opacity: 1;
  transition: opacity 0.3s;
}

input:checked + .toggle-label::before {
  content: "ON";
  left: auto;
  right: 5px;
  opacity: 0; /* Hide OFF text */
}

input:checked + .toggle-label::after {
  transform: translateX(24px);
}

input:checked + .toggle-label::before {
  opacity: 1; /* Show ON text */
}

Adjust the positioning logic to ensure the text moves smoothly with the circle or stays fixed depending on your design preference.

Verify the installation

Open your HTML file in a browser. Click the toggle switch. You should see the track change color from gray to green and the white circle slide from left to right. Inspect the element in the browser DevTools to confirm the `input` is hidden but the `label` is styled correctly. Try using the Tab key to focus the label and pressing Enter to toggle it. This confirms keyboard accessibility.

Troubleshooting

Error: The switch does not change color on click. Check that the `for` attribute in the label matches the `id` of the input exactly. Ensure the input is not inside a shadow DOM or an iframe without proper context. Verify that the `input:checked` selector is correctly chained with the `+` combinator.

Error: The circle moves too slowly or jitters. Reduce the `transition` duration on the `::after` pseudo-element. Avoid animating `width` or `height` properties; use `transform` instead. Ensure no other CSS rules are overriding your `transition` properties.

Error: The switch is not accessible on mobile. Ensure the `label` has a large enough touch target. Do not remove the `cursor: pointer` property. Test on a real mobile device to ensure the touch events trigger the click event on the hidden input.

Error: The text overlaps the circle. Adjust the `left` and `right` values of the `::before` pseudo-element. Use `calc()` to dynamically position the text relative to the circle width. Ensure the font size is small enough to fit within the track boundaries.

Error: The switch resets on page reload. This is expected behavior for standard checkboxes. To persist the state, you must use `localStorage` or `sessionStorage`, which requires JavaScript. Since this tutorial is for pure CSS, the state will reset on refresh, which is the correct behavior for a standard form control.

/* Final complete code block */
.switch-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background: #f4f4f4;
}

.toggle {
  display: none;
}

.toggle-label {
  position: relative;
  display: inline-block;
  width: 50px;
  height: 26px;
  background-color: #ccc;
  border-radius: 34px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.toggle-label::before {
  content: "OFF";
  position: absolute;
  left: 5px;
  top: 50%;
  transform: translateY(-50%);
  color: #333;
  font-size: 10px;
  font-weight: bold;
  transition: opacity 0.3s;
}

.toggle-label::after {
  content: "";
  position: absolute;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  top: 1px;
  left: 1px;
  background-color: white;
  transition: left 0.3s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

input:checked + .toggle-label {
  background-color: #4CAF50;
}

input:checked + .toggle-label::before {
  content: "ON";
  left: auto;
  right: 5px;
  color: white;
}

input:checked + .toggle-label::after {
  transform: translateX(24px);
}
Sponsored

Powerful Dedicated Servers — Linux & Windows

Bare-metal performance with SSD storage, DDoS protection and 24/7 expert support. Ideal for production workloads, databases and high-traffic sites.

Tags: CSSHTMLFrontendWeb DesignAccessibility
0
Was this helpful?

Related tutorials

Comments 0

Login to leave a comment.

No comments yet — be the first to share your thoughts.