Stretchy Toggle

The knob stretches into a pill as it travels, then settles round on the other side while the track fills with colour.

  • CSS
  • Forms & controls
  • Toggle
  • Added

AI prompt

Make an accessible React toggle switch in CSS. Use a real <button role="switch" aria-checked> with a visible text label "Email notifications" next to it. The track is 48×28px; the 22px knob sits at translateX(3px) when off and translateX(23px) when on, moving over 260ms with cubic-bezier(0.16, 1, 0.3, 1). While pressed (:active) the knob widens by scaling scaleX(1.25) from the side it is leaving, so it looks like it stretches. The track fades from a neutral surface to the accent colour over 200ms. Show a visible focus ring on :focus-visible. Clicking the label also toggles it. Under prefers-reduced-motion, move the knob instantly and skip the stretch.

Source

Demo.tsx
'use client'

import { useId, useState } from 'react'
import s from './demo.module.css'

export default function ToggleSwitch({ labels }: { labels: Record<string, string> }) {
  const [on, setOn] = useState(true)
  const id = useId()

  return (
    <div className={s.row}>
      <label htmlFor={id} className={s.label}>
        {labels.label}
      </label>
      <button id={id} type="button" role="switch" aria-checked={on} className={s.track} onClick={() => setOn(!on)}>
        <span className={s.knob} />
      </button>
    </div>
  )
}
demo.module.css
.row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 18px;
  border: 1px solid var(--stage-rule);
  border-radius: 14px;
  background: var(--stage-surface);
}

.label { color: var(--stage-ink); font: 500 var(--text-base) / 1.2 var(--font-body); white-space: nowrap; cursor: pointer; }

.track {
  position: relative;
  flex: none;
  width: 48px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--stage-rule);
  cursor: pointer;
  transition: background-color 200ms var(--ease-out);
}

.track[aria-checked='true'] { background: var(--stage-accent); }
.track:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

.knob {
  position: absolute;
  top: 3px;
  left: 0;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  background: var(--stage-ink);
  transform: translateX(3px);
  transform-origin: left;
  transition: transform 260ms var(--ease-out);
}

.track[aria-checked='true'] .knob { transform: translateX(23px); transform-origin: right; }
.track:active .knob { transform: translateX(3px) scaleX(1.25); }
.track[aria-checked='true']:active .knob { transform: translateX(23px) scaleX(1.25); }

@media (prefers-reduced-motion: reduce) {
  .knob, .track { transition: none; }
  .track:active .knob { transform: translateX(3px); }
  .track[aria-checked='true']:active .knob { transform: translateX(23px); }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.