AI CSS Animations

On Off Label Morph Toggle

The word inside the track morphs instead of swapping: the shared O glides to the other side while the ff collapses with a blur and an n grows in its place. Letter spans and max-width.

Also known as: on off text morph switch, toggle switch text animation, on off label toggle css, morphing text toggle.

HTML

<label class="tgl-label-morph-toggle">
  <input type="checkbox" role="switch" aria-label="Autosave">
  <span class="tgl-label-morph-toggle-track" aria-hidden="true">
    <span class="tgl-label-morph-toggle-word"><span>O</span><span class="tgl-label-morph-toggle-n">n</span><span class="tgl-label-morph-toggle-ff">ff</span></span>
    <span class="tgl-label-morph-toggle-knob"></span>
  </span>
</label>

CSS

.tgl-label-morph-toggle {
  --c: var(--tgl-color, #3b82f6);
  font-size: var(--tgl-size, 24px);
  position: relative;
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  font-family: system-ui, sans-serif;
  line-height: 1.2;
}
.tgl-label-morph-toggle input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.tgl-label-morph-toggle-track {
  position: relative;
  display: block;
  width: 4.4em;
  height: 2.1em;
  border-radius: 99em;
  background: color-mix(in srgb, currentColor 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 8%, transparent);
  transition: background-color 0.35s ease;
}
.tgl-label-morph-toggle-knob {
  position: absolute;
  top: 0.2em;
  left: 0.2em;
  width: 1.7em;
  height: 1.7em;
  border-radius: 99em;
  background: #fff;
  box-shadow: 0 0.12em 0.3em rgb(0 0 0 / 0.25);
  transition: transform 0.5s cubic-bezier(0.34, 1.3, 0.5, 1), width 0.2s ease;
}
.tgl-label-morph-toggle-word {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: center;
  font-weight: 700;
  color: color-mix(in srgb, currentColor 55%, transparent);
  transform: translateX(2.2em);
  transition: transform 0.5s cubic-bezier(0.34, 1.2, 0.5, 1), color 0.3s ease;
}
.tgl-label-morph-toggle-word > span {
  display: inline-block;
  overflow: hidden;
  font-size: 0.78em;
  white-space: nowrap;
  transition: max-width 0.45s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.3s ease, filter 0.3s ease;
}
.tgl-label-morph-toggle-n {
  max-width: 0;
  opacity: 0;
  filter: blur(0.15em);
}
.tgl-label-morph-toggle-ff {
  max-width: 1em;
}
.tgl-label-morph-toggle:active .tgl-label-morph-toggle-knob {
  width: 2.05em;
}
.tgl-label-morph-toggle input:checked + .tgl-label-morph-toggle-track {
  background: var(--c);
}
.tgl-label-morph-toggle input:checked + .tgl-label-morph-toggle-track .tgl-label-morph-toggle-knob {
  transform: translateX(2.3em);
}
.tgl-label-morph-toggle:active input:checked + .tgl-label-morph-toggle-track .tgl-label-morph-toggle-knob {
  transform: translateX(1.95em);
}
.tgl-label-morph-toggle input:checked + .tgl-label-morph-toggle-track .tgl-label-morph-toggle-word {
  color: #fff;
  transform: translateX(0.8em);
}
.tgl-label-morph-toggle input:checked + .tgl-label-morph-toggle-track .tgl-label-morph-toggle-n {
  max-width: 0.7em;
  opacity: 1;
  filter: none;
}
.tgl-label-morph-toggle input:checked + .tgl-label-morph-toggle-track .tgl-label-morph-toggle-ff {
  max-width: 0;
  opacity: 0;
  filter: blur(0.15em);
}
.tgl-label-morph-toggle input:focus-visible + .tgl-label-morph-toggle-track {
  outline: 0.12em solid var(--c);
  outline-offset: 0.2em;
}
@media (prefers-reduced-motion: reduce) {
  .tgl-label-morph-toggle *,
  .tgl-label-morph-toggle *::before,
  .tgl-label-morph-toggle *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tgl-label-morph-toggle, .tgl-label-morph-toggle *, .tgl-label-morph-toggle::before, .tgl-label-morph-toggle::after, .tgl-label-morph-toggle *::before, .tgl-label-morph-toggle *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Customizing this snippet

  • Color: set --tgl-color on the element or a parent. Currently previewing #3b82f6.
  • Size: set --tgl-size. Every other dimension is in em, so the whole thing scales together. Currently previewing 24px.
  • Speed: change the duration on the transition or animation shorthand. Staggered delays are relative, so they keep their rhythm.
  • Reduced motion: the rule at the end of the CSS turns the motion off for people who asked their OS for less of it. Keep it.

Similar toggle switches