<main>
  <h1>Better Motion</h1>
  <p>Be sure to keep accessibility in mind when designing for motion</p>
  <button data-toggle hidden role=switch aria-checked=true>
    <span class="button__text">
      Toggle motion<span aria-hidden=true data-btn-text>: On</span></span>
    <span class="button__toggle"></span>
  </button>
</main>

<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
* {
  box-sizing: border-box;
}

:root {
  --buttonColor: #ff1fb0;
}

body {
  font-family: 'Spectral', serif;
  font-size: clamp(100%, 1rem + 2vw, 2rem);
  margin: 0;
  background: #2c67d4;
  color: #ffffff;
  min-height: 100vh;
  max-width: 100%;
  overflow-x: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
}

main {
  width: 100%;
  max-width: 86rem;
  padding: 2rem;
  position: relative;
  z-index: 1;
}

h1 {
  font-weight: 200;
  font-size: clamp(4rem, 10vw, 12rem);
  line-height: 1.2;
  letter-spacing: -0.07em;
  margin: 0;
}

p {
  margin: 0 0 2rem;
}

button {
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  padding: 1rem 1.5rem;
  border-radius: 0.4rem;
  font-size: 1.15rem;
  font-family: Helvetica, sans-serif;
  font-weight: 600;
  background-color: var(--buttonColor);
  color: inherit;
  box-shadow: 0 0.1rem 0.85rem rgba(0, 0, 0, 0.25);
  cursor: pointer;
  min-width: 16rem;
  transition: color 200ms, background-color 200ms;
}

button:is(:hover, :focus) {
  --buttonColor: #bf0d81;
}

.button__toggle {
  display: block;
  position: relative;
  flex: 0 0 2.4rem;
  height: 1.5rem;
  border: 2px solid;
  border-radius: 1.5rem;
  margin-left: 0.5rem;
  padding: 4px;
  transform: background-color 100ms 200ms;
}

.button__toggle::after {
  position: absolute;
  width: calc(1.5rem - 10px);
  height: calc(1.5rem - 10px);
  top: 3px;
  left: 3px;
  background-color: #ffffff;
  content: '';
  border-radius: 50%;
  transition: transform 300ms, background-color 300ms;
  transform-origin: center center;
}

.button__text {
  flex: 0 0 auto;
}

.circle {
  --c1: #f2c4e2;
  --c2: #d97eb9;
  --size: max(8rem, 13vw);
  --delay: 0s;
  position: absolute;
  top: 0;
  left: 0;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  background: radial-gradient(circle at 20% 70%, var(--c1), var(--c2));
  pointer-events: none;
  animation: var(--animation, drift) 12s var(--delay) linear infinite alternate;
  animation-play-state: var(--playState, paused);
}

@media (prefers-reduced-motion: no-preference) {
  body {
    --playState: running;
  }
}

.allow-motion .button__toggle {
  background-color: #ffffff;
}

.allow-motion .button__toggle::after {
  transform: translate3d(100%, 0, 0) scale(1.2);
  background-color: var(--buttonColor);
}

/* Circle animations */
.circle:nth-child(2) {
  --c1: #194aa6;
  --c2: #194aa6;
  --size: max(3rem, 10vw);
  --delay: 1s;
  top: 50%;
  left: 25%;
}

.circle:nth-child(3) {
  --c1: #194aa6;
  --c2: #194aa6;
  --size: max(3rem, 10vw);
  --delay: 3s;
  top: 30%;
  left: 50%;
}

.circle:nth-child(4) {
  --c1: #a0e3f2;
  --c2: #2fa7c2;
  --animation: drift2;
  --delay: 5s;
  top: 70%;
  left: 20%;
}

.circle:nth-child(5) {
  --animation: drift2;
  --delay: 4s;
  top: 60%;
  left: 70%;
}

.circle:nth-child(6) {
  --c1: #a0e3f2;
  --c2: #2fa7c2;
  --delay: 7s;
  top: 15%;
  left: 80%;
}


@keyframes drift {
  50% {
    transform: translate3d(50%, 50%, 0);
  }
  100% {
    transform: translate3d(0, 100%, 0);
  }
}

@keyframes drift2 {
  50% {
    transform: translate3d(-50%, -50%, 0);
  }
  100% {
    transform: translate3d(0, -100%, 0);
  }
}

const toggle = document.querySelector('[data-toggle]')
const buttonText = document.querySelector('[data-btn-text]')
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')

const setReducedMotionStyles = () => {
  buttonText.innerText = ': Off'
  toggle.setAttribute('aria-checked', 'false')
  document.body.classList.remove('allow-motion')
  document.body.style.setProperty('--playState', 'paused')
}

const setMotionStyles = () => {
  buttonText.innerText = ': On'
  toggle.setAttribute('aria-checked', 'true')
  document.body.classList.add('allow-motion')
  document.body.style.setProperty('--playState', 'running')
}

const getMotionPreference = () => {
  const localStoragePreference = localStorage.getItem('prefersReducedMotion')
  
  // First check localStorage, to see if the user has previously stated a preference
  if (localStoragePreference) {
    return localStoragePreference
  } else {
    // Otherwise, check the user's system preferences
    return prefersReducedMotion.matches ? 'reduce' : 'no-preference'
  }
}

const setInitialStyles = () => {
  // Otherwise, check the user's system preferences
  if (getMotionPreference() == 'reduce') {
    setReducedMotionStyles()
  } else {
    setMotionStyles()
  }
  
  // The button is hidden initially, as it won't work without JS. We need to make it visible
  toggle.hidden = false
}

setInitialStyles()

toggle.addEventListener('click', () => {
  if (getMotionPreference() == 'reduce') {
    // If currently set to reduced motion, toggle motion on
    localStorage.setItem('prefersReducedMotion', 'no-preference')
    setMotionStyles()
  } else {
    // Otherwise, toggle motion off
    localStorage.setItem('prefersReducedMotion', 'reduce')
    setReducedMotionStyles()
  }
})

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.