HTML
<div class="swappy-radios" role="radiogroup" aria-labelledby="swappy-radios-label">
<h3 id="swappy-radios-label">Select an option</h3>
<label>
<input type="radio" name="options" checked />
<span class="radio"></span>
<span>First option</span>
</label>
<label>
<input type="radio" name="options" />
<span class="radio"></span>
<span>Second option</span>
</label>
<label>
<input type="radio" name="options" />
<span class="radio"></span>
<span>Third option</span>
</label>
<label>
<input type="radio" name="options" />
<span class="radio"></span>
<span>Fourth option</span>
</label>
<label>
<input type="radio" name="options" />
<span class="radio"></span>
<span>Last option</span>
</label>
</div>
CSS
html {
box-sizing: border-box;
height: 100%;
font-size: 10px;
}
*, *::before, *::after {
box-sizing: inherit;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
color: #0f273d;
font-family: 'Lato', sans-serif;
}
h3 {
font-size: 2.5rem;
font-weight: bold;
}
.swappy-radios label {
display: block;
position: relative;
padding-left: 4rem;
margin-bottom: 1.5rem;
cursor: pointer;
font-size: 2rem;
user-select: none;
color: #555;
}
.swappy-radios label:hover input ~ .radio {
opacity: 0.8;
}
.swappy-radios input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.swappy-radios input:checked ~ span {
color: #0bae72;
transition: color .5s;
}
.swappy-radios input:checked ~ .radio {
background-color: #0ac07d;
opacity: 1 !important;
}
.swappy-radios input:checked ~ .radio::after {
opacity: 1;
}
.swappy-radios .radio {
position: absolute;
top: 0;
left: 0;
height: 2.5rem;
width: 2.5rem;
background: #c9ded6;
border-radius: 50%;
}
.swappy-radios .radio::after {
display: block;
content: '';
position: absolute;
opacity: 0;
top: .5rem;
left: .5rem;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: #fff;
}
JS
let currentValue = 1;
const timeout = 0.75;
const radios = document.querySelectorAll('.swappy-radios input');
const fakeRadios = document.querySelectorAll('.swappy-radios .radio');
console.log(document.querySelector('.swappy-radios label:nth-of-type(1) .radio'));
const firstRadioY = document.querySelector('.swappy-radios label:nth-of-type(1) .radio').getBoundingClientRect().y;
const secondRadioY = document.querySelector('.swappy-radios label:nth-of-type(2) .radio').getBoundingClientRect().y;
const indicitiveDistance = secondRadioY - firstRadioY;
fakeRadios.forEach(function(radio) {
radio.style.cssText = `transition: background 0s ${timeout}s;`;
});
const css = `.radio::after {transition: opacity 0s ${timeout}s;}`
const head = document.head;
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
head.appendChild(style);
radios.forEach(function(radio, i) {
radio.parentElement.setAttribute('data-index', i + 1);
//The meat: set up the change listener!
radio.addEventListener('change', function() {
temporarilyDisable();
removeStyles();
const nextValue = this.parentElement.dataset.index;
const oldRadio = document.querySelector(`[data-index="${currentValue}"] .radio`);
const newRadio = this.nextElementSibling;
const oldRect = oldRadio.getBoundingClientRect();
const newRect = newRadio.getBoundingClientRect();
const yDiff = Math.abs(oldRect.y - newRect.y);
const dirDown = oldRect.y - newRect.y > 0 ? true : false;
const othersToMove = [];
const lowEnd = Math.min(currentValue, nextValue);
const highEnd = Math.max(currentValue, nextValue);
const inBetweenies = range(lowEnd, highEnd, dirDown);
let othersCss = '';
inBetweenies.map(option => {
const staggerDelay = inBetweenies.length > 1 ? 0.1 / inBetweenies.length * option : 0;
othersCss += `
[data-index="${option}"] .radio {
animation: moveOthers ${timeout - staggerDelay}s ${staggerDelay}s;
}
`;
});
const css = `
${othersCss}
[data-index="${currentValue}"] .radio {
animation: moveIt ${timeout}s;
}
@keyframes moveIt {
0% { transform: translateX(0); }
33% { transform: translateX(-3rem) translateY(0); }
66% { transform: translateX(-3rem) translateY(${dirDown ? '-' : ''}${yDiff}px); }
100% { transform: translateX(0) translateY(${dirDown ? '-' : ''}${yDiff}px); }
}
@keyframes moveOthers {
0% { transform: translateY(0); }
33% { transform: translateY(0); }
66% { transform: translateY(${dirDown ? '' : '-'}${indicitiveDistance}px); }
100% { transform: translateY(${dirDown ? '' : '-'}${indicitiveDistance}px); }
}
`;
appendStyles(css);
currentValue = nextValue;
});
});
function appendStyles(css) {
const head = document.head;
const style = document.createElement('style');
style.type = 'text/css';
style.id = 'swappy-radio-styles';
style.appendChild(document.createTextNode(css));
head.appendChild(style);
}
function removeStyles() {
const node = document.getElementById('swappy-radio-styles');
if (node && node.parentNode) {
node.parentNode.removeChild(node);
}
}
function range(start, end, dirDown) {
let extra = 1;
if (dirDown) {
extra = 0;
}
return [...Array(end - start).keys()].map(v => start + v + extra);
}
function temporarilyDisable() {
radios.forEach((item) => {
item.setAttribute('disabled', true);
setTimeout(() => {
item.removeAttribute('disabled');
}, timeout * 1000);
});
}
Источник: https://codepen.io/liamj/pen/NegxNB