Back to the library
Reorder, flat swap
Variant: Flat swap
The same reorder with no elevation at all. Two rows still have to share a band on the way past, and with nothing lifted above the plane the crossing gets resolved sideways: the travelling row parts 10px and drops to half opacity while it moves. A slot change costs 256ms and nothing has to rise and fall, which is why the loop does two of them back to back.
controllistgesture
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reorder — flat swap</title>
<style>
:root {
--ivory: #faf9f5;
--slate: #141413;
--clay: #d97757;
--oat: #e3dacc;
--paper: #ffffff;
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--ease-sine: cubic-bezier(0.4, 0, 0.6, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
/* Two swaps in one 3.2s loop, then a dissolve to reset. Doing it twice is
the point: with no lift to raise and lower, a slot change costs 256ms
and can repeat straight away — nothing leaves the plane of the list. */
.list {
position: relative;
width: 190px;
height: 118px;
animation: list-cycle 3.2s linear infinite;
}
.row {
position: absolute;
left: 0;
width: 190px;
height: 34px;
}
/* Stacking is declared, not inherited from source order. Nothing is lifted
here, so the crossing has to resolve somewhere: the travelling row goes
behind the rows it trades with, every time, in every browser. Leaving
that to DOM order works right up until someone reorders the markup. */
.r1 {
top: 0;
z-index: 1;
animation: fall-two 3.2s infinite;
}
.r2 {
top: 42px;
z-index: 2;
animation: rise-first 3.2s infinite;
}
.r3 {
top: 84px;
z-index: 2;
animation: rise-second 3.2s infinite;
}
.card {
position: relative;
height: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 0 12px;
border-radius: 8px;
background: var(--paper);
border: 1px solid var(--oat);
}
.r1 .card {
animation: part-right 3.2s infinite;
}
.r2 .card {
animation: part-left-first 3.2s infinite;
}
.r3 .card {
animation: part-left-second 3.2s infinite;
}
.grip {
width: 8px;
height: 14px;
flex: none;
color: rgba(20, 20, 19, 0.34);
background-image: radial-gradient(circle, currentColor 1.1px, transparent 1.3px);
background-size: 4px 5px;
}
.r1 .grip {
color: var(--clay);
}
.bar {
height: 6px;
border-radius: 3px;
background: rgba(20, 20, 19, 0.16);
}
.r1 .bar {
width: 74px;
}
.r2 .bar {
width: 94px;
}
.r3 .bar {
width: 58px;
}
.pointer {
position: absolute;
left: 3px;
top: 50%;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(20, 20, 19, 0.11);
translate: 0 -50%;
opacity: 0;
animation: press 3.2s infinite;
}
@keyframes press {
0%,
5% {
opacity: 0;
transform: scale(1.35);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
8%,
38% {
opacity: 1;
transform: scale(1);
}
44%,
100% {
opacity: 0;
transform: scale(1);
}
}
/* The neighbour leaves 64ms before the dragged row starts and lands 112ms
before it does, so the two are only in the same band briefly. Ease-in-out
because both ends are on-screen positions — nothing is entering here. */
@keyframes fall-two {
0%,
10% {
transform: translateY(0);
animation-timing-function: var(--ease-in-out);
}
18%,
30% {
transform: translateY(42px);
animation-timing-function: var(--ease-in-out);
}
38%,
100% {
transform: translateY(84px);
}
}
@keyframes rise-first {
0%,
8% {
transform: translateY(0);
animation-timing-function: var(--ease-in-out);
}
14.5%,
100% {
transform: translateY(-42px);
}
}
@keyframes rise-second {
0%,
28% {
transform: translateY(0);
animation-timing-function: var(--ease-in-out);
}
34.5%,
100% {
transform: translateY(-42px);
}
}
/* Two rows swapping have to share a band on the way past. With no lift to
put one above the other, the crossing is resolved on the same plane: the
travelling row parts 10px sideways and drops to half opacity while it
moves, so it reads as passing rather than colliding. */
@keyframes part-right {
0%,
10% {
transform: translateX(0);
opacity: 1;
animation-timing-function: var(--ease-sine);
}
14% {
transform: translateX(10px);
opacity: 0.5;
animation-timing-function: var(--ease-sine);
}
18%,
30% {
transform: translateX(0);
opacity: 1;
animation-timing-function: var(--ease-sine);
}
34% {
transform: translateX(10px);
opacity: 0.5;
animation-timing-function: var(--ease-sine);
}
38%,
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes part-left-first {
0%,
8% {
transform: translateX(0);
opacity: 1;
animation-timing-function: var(--ease-sine);
}
11.5% {
transform: translateX(-10px);
opacity: 0.75;
animation-timing-function: var(--ease-sine);
}
14.5%,
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes part-left-second {
0%,
28% {
transform: translateX(0);
opacity: 1;
animation-timing-function: var(--ease-sine);
}
31.5% {
transform: translateX(-10px);
opacity: 0.75;
animation-timing-function: var(--ease-sine);
}
34.5%,
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes list-cycle {
0% {
opacity: 0;
}
4%,
88% {
opacity: 1;
}
96%,
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.list,
.row {
animation: none;
opacity: 1;
transform: none;
}
.r1 .card,
.r2 .card,
.r3 .card {
animation: none;
opacity: 1;
transform: none;
}
/* Travel gone, so the reorder is told in colour instead: the clay handle
marking the held row hands off to the row it traded with. Same fact,
no displacement. */
.r1 .grip {
animation: grip-cool 3.2s ease infinite;
}
.r2 .grip {
animation: grip-warm 3.2s ease infinite;
}
@keyframes grip-cool {
0%,
18% {
color: var(--clay);
}
34%,
62% {
color: rgba(20, 20, 19, 0.34);
}
82%,
100% {
color: var(--clay);
}
}
@keyframes grip-warm {
0%,
18% {
color: rgba(20, 20, 19, 0.34);
}
34%,
62% {
color: var(--clay);
}
82%,
100% {
color: rgba(20, 20, 19, 0.34);
}
}
.pointer {
animation: hold-press 3.2s ease infinite;
transform: none;
}
@keyframes hold-press {
0%,
6% {
opacity: 0;
}
18%,
58% {
opacity: 1;
}
74%,
100% {
opacity: 0;
}
}
}
</style>
</head>
<body>
<div class="list" role="img" aria-label="Two list rows exchanging places on a flat plane">
<div class="row r1">
<div class="card">
<span class="grip"></span>
<span class="bar"></span>
<span class="pointer"></span>
</div>
</div>
<div class="row r2">
<div class="card">
<span class="grip"></span>
<span class="bar"></span>
</div>
</div>
<div class="row r3">
<div class="card">
<span class="grip"></span>
<span class="bar"></span>
</div>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.