<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()
}
})
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.