Toggle (CSS Only Trick)

A fully CSS toggle built on a visually hidden checkbox and the :has() selector. Checking the box shifts the dot with a bouncy transition, changes the label color, and changes the section background — all without JavaScript.

csstogglecheckboxhasno-javascriptanimation

Code

index.html
html
<section class="section-resource">
  <div class="btn-toggle">
    <div class="btn-toggle__toggle">
      <div class="btn-toggle__toggle-dot"></div>
    </div>
    <p class="btn-toggle__p">I'm a toggle</p>
    <input type="checkbox" class="btn-toggle__checkbox">
  </div>
</section>
styles.css
css
.section-resource {
  background-color: #e4d8d5;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  font-size: 4em;
  display: flex;
  transition: background-color 0.4s cubic-bezier(0.35, 1, 0.6, 1);
}

.btn-toggle {
  grid-column-gap: .5em;
  grid-row-gap: .5em;
  cursor: pointer;
  align-items: center;
  display: flex;
  position: relative;
}

.btn-toggle__toggle {
  background-color: #ff4c24;
  border-radius: 2em;
  flex-shrink: 0;
  width: 2em;
  height: 1.25em;
  display: flex;
}

.btn-toggle__toggle-dot {
  background-color: #fff;
  border-radius: 50%;
  width: 1em;
  height: 1em;
  margin-top: .125em;
  margin-left: .125em;
  transition: all 0.4s cubic-bezier(0.35, 1.5, 0.6, 1);
  transform: translateX(0em) rotate(0.001deg);
}

.btn-toggle__p {
  margin-bottom: 0;
  font-size: 1.25em;
  line-height: 1;
  position: relative;
  transition: color 0.4s cubic-bezier(0.35, 1, 0.6, 1);
}

.btn-toggle__checkbox {
  opacity: 0;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 100%;
  position: absolute;
  cursor: pointer;
}

@media screen and (max-width: 767px) {
  .section-resource {
    font-size: 7vw;
  }
}

/* Animations based on :checked */

.section-resource:has(input[type="checkbox"]:checked) {
  background: #301139;
}

.btn-toggle:has(input[type="checkbox"]:checked) .btn-toggle__toggle-dot {
  transform: translateX(0.75em) rotate(0.001deg);
}

.btn-toggle:has(input[type="checkbox"]:checked) .btn-toggle__p {
  color: #EFEEEC;
}

Guide

The :has() Trick

The checkbox is hidden with opacity: 0 and stretched over the entire toggle using position: absolute and width/height 100%, so clicking anywhere on the component toggles it. :has(input:checked) on a parent selector then drives all visual changes in pure CSS.

Dot Movement

The dot uses translateX(0.75em) when checked to slide to the right. The cubic-bezier(0.35, 1.5, 0.6, 1) overshoot gives it a bouncy landing. rotate(0.001deg) is a GPU compositing hint to keep the animation smooth.

Extending It

Any element inside or outside the toggle can be styled based on the checked state as long as you can reach it with a :has() selector on a shared ancestor. Change colours, swap icons, show/hide elements — all without JavaScript.