screenshot of a toggle switch on codepen

Create the perfect toggle switch with pure HTML / CSS, no JavaScript necessary

Based on input element type checkbox, styled with CSS pseudo-elements ::before and ::after and pseudo-class :checked

5 min readMar 16, 2021



This is the toggle switch that will be created:

animated toggle switch
  • an elliptical toggle switch in pure HTML and CSS (no JavaScript necessary)
  • when toggled (click): change of color and text content
  • based on/use of the HTML element <input type="checkbox"> , here, modification through CSS styling, also known as “checkbox hack”
  • a bigger hit area through the use of the associated HTML element <label> which enables toggling the switch while clicking the text next to it— not only the tiny little toggle switch itself— this makes it easier to toggle between options


The HTML element <input type="checkbox"> brings the desired behavior out of the box — it is rendered by default as a box that is checked or unchecked when clicked/ticked. The default appearance of the HTML element <input type="checkbox"> depends on the browser, see the default appearance for a checkbox in Chrome below. While styling our toggle switch, these default stylings will be overwritten.

<input type=”checkbox”> with :focus and :active state and :checked attribute for the first checkbox
<input type="checkbox"> with :focus and :active state and :checked attribute for the first checkbox

The pseudo-class :checked allows to turn a single value on and off. Present as an attribute within HTML element <input type="checkbox" checked> the checkbox is toggled on by default. For our toggle switch, the :checked won’t be present within the HTML but will be used as pseudo-class within the CSS.

Read more about the HTML element input type checkbox on MDN Web Docs

Not into reading further?

See final CodePen here:


<div class="toggle-checkbox-wrapper">
<input class="toggle-checkbox" type="checkbox" id="toggle">
<label class="slider" for="toggle">
<span class="toggle-switch opt1">Look, I'm blue!</span>
<span class="toggle-switch opt2">Look, I'm brown!</span>
  • the outer <div> element is used to wrap the <input> element and <label> element — it’s nicer to have all of the toggle switch in a dedicated container
  • the <input> element needs to have the id attribute to make the toggle switch work
  • the <label> element needs to have the for attribute to make the toggle switch work
  • the id attribute and the for attribute both need to have the same value, this “connects” the <input> element to the <label> element
  • the <label> element must appear after the <input> element
  • two <span> elements are needed, as there will be two texts to toggle between
  • clicking on the <label> element generally enables a bigger hit area, in our case the size of this hit area depends on the length of the text within the <span>(s)


* {
box-sizing: border-box;
margin: 0;
padding: 0;
body {
background-color: #F5DEB3;
color: #282828;
font-family: arial, helvetica, sans-serif;
font-size: 16px;
font-weight: 700;
padding: 24px;
.toggle-checkbox-wrapper {
background-color: #fff;
padding: 24px;
  • reset * is set to get the usual box-sizing, margin and padding issues out of the way
  • body sets very general stylings and the font
  • .toggle-checkbox-wrapper is the outer <div> element styling, just minimal styling not necessary for the toggle switch itself

Styling on the <input> element:

.toggle-checkbox {
display: none;
  • .toggle-checkbox { display: none; } on the <input> element hides the default checkbox appearance, see Chrome example above

Styling on the <label> element:

slider {
position: relative;
/* ellipsis */
.slider::before {
background: lightblue;
border-radius: 34px;
bottom: 0;
content: '';
height: 24px;
margin: auto;
position: absolute;
top: 0;
width: 40px;
/* circle */
.slider::after {
background: navy;
border-radius: 50%;
bottom: 0;
content: '';
height: 16px;
left: 4px;
margin: auto;
position: absolute;
top: 0;
transition: 0.4s;
width: 16px;
  • .slider on the <label> element in a broader sense is the ellipsis but — important! — doesn’t get a lot of styling in our case
  • instead, the ellipsis and the circle will be created and styled via pseudo-element .slider::before for the ellipsis and pseudo-element .slider::after for the circle
  • the default for both, ellipsis and circle, is the blue version
  • in these pseudo-classes shared stylings that both the brown and the blue version use are put

Styling on the <span> elements:

.toggle-switch {
margin-left: 50px;
.toggle-switch.opt1 {
color: navy;
.toggle-switch.opt2 {
color: brown;
display: none;
  • .toggle-switch on both <span>s has .margin-left: 50px; to distance the text from the ellipsis
  • .toggle-switch.opt1 on first <span> gets blue text color .color: navy;
  • .toggle-switch.opt2 on second<span> gets brown text color .color: brown; and display: none; to hide this <span>otherwise you would see both texts

Back to the <input> element:

.toggle-checkbox:checked + .slider::before {
background-color: lightsalmon;
.toggle-checkbox:checked + .slider::after {
background-color: maroon;
transform: translateX(16px);
  • .toggle-checkbox:checked + .slider::before will toggle the lightblue background-color of the ellipsis
  • .toggle-checkbox:checked + .slider::after will toggle the lightbrown background-color of the circle
  • transform: translateX(16px); will push the brown circle 16px to the right on the x axis
.toggle-checkbox:checked ~ .slider > .toggle-switch.opt1 {
display: none;
.toggle-checkbox:checked ~ .slider > .toggle-switch.opt2 {
display: inline-block;
  • display: none; will — on click — hide the blue version
  • display: inline-block; will — on click — display the brown version
  • and vice versa


After wrapping my head around this and taking the time to dig into it I was honestly surprised how easy this is and how little code is needed to create a perfect toggle switch.

Actually, I’ve coded this in SCSS which in my opinion makes it even easier to understand what’s happening.

Only need a very basic toggle switch? I got you covered, see here: