Back to the library
Reorder, flick and overshoot
Variant: Flick and overshoot
A flicked row rather than a dragged one. The pointer leaves before the row has gone anywhere, and momentum carries it 6px past the slot and leans it 1.6 degrees before it decays back; the neighbour, only displaced, overshoots half as far. The 432ms settle is the one place in this set worth going past 300ms — the response you actually judge it by lands in 144ms.
listgesture
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 — flick and overshoot</title>
<style>
:root {
--ivory: #faf9f5;
--slate: #141413;
--clay: #d97757;
--oat: #e3dacc;
--paper: #ffffff;
--ease-out: cubic-bezier(0.23, 1, 0.32, 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);
}
.list {
position: relative;
width: 190px;
height: 118px;
animation: list-cycle 3.2s linear infinite;
}
.row {
position: absolute;
left: 0;
width: 190px;
height: 34px;
}
/* The thrown row passes over its neighbour, so it holds the higher stacking
context for the whole cycle — a flicked object goes above, never through. */
.r1 {
top: 0;
z-index: 2;
animation: throw 3.2s infinite;
}
.r2 {
top: 42px;
animation: displaced 3.2s infinite;
}
.r3 {
top: 84px;
}
.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: lean 3.2s infinite;
}
.shadow {
position: absolute;
inset: -1px;
border-radius: 9px;
box-shadow: 0 8px 18px -8px rgba(20, 20, 19, 0.34);
opacity: 0;
animation: airborne 3.2s infinite;
}
.grip,
.bar {
position: relative;
}
.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: flick-press 3.2s infinite;
}
/* The pointer leaves at 9.5% — before the row has gone anywhere. Everything
after that is the row travelling on the momentum it was given, which is
the only reason a 432ms settle is allowed on a list this small: the
response you judge it by happened in the first 144ms. */
@keyframes flick-press {
0%,
5% {
opacity: 0;
transform: scale(1.35);
animation-timing-function: var(--ease-out);
}
7.5%,
9.5% {
opacity: 1;
transform: scale(1);
animation-timing-function: var(--ease-out);
}
12%,
100% {
opacity: 0;
transform: scale(1.15);
}
}
@keyframes throw {
0%,
9.5% {
transform: translateY(0);
animation-timing-function: var(--ease-out);
}
14% {
transform: translateY(48px);
animation-timing-function: var(--ease-sine);
}
17.5% {
transform: translateY(40px);
animation-timing-function: var(--ease-sine);
}
20.5%,
100% {
transform: translateY(42px);
}
}
/* 6px of overshoot on the thrown row against 3px on the neighbour. The
neighbour was displaced, not flicked, so it carries half the energy. */
@keyframes displaced {
0%,
9% {
transform: translateY(0);
animation-timing-function: var(--ease-out);
}
13.5% {
transform: translateY(-45px);
animation-timing-function: var(--ease-sine);
}
16.5% {
transform: translateY(-41.5px);
animation-timing-function: var(--ease-sine);
}
19%,
100% {
transform: translateY(-42px);
}
}
@keyframes lean {
0%,
7% {
transform: scale(1) rotate(0deg);
animation-timing-function: var(--ease-out);
}
9.5% {
transform: scale(1.02) rotate(0deg);
animation-timing-function: var(--ease-out);
}
14% {
transform: scale(1.02) rotate(1.6deg);
animation-timing-function: var(--ease-sine);
}
20.5%,
100% {
transform: scale(1) rotate(0deg);
}
}
@keyframes airborne {
0%,
8% {
opacity: 0;
animation-timing-function: var(--ease-out);
}
12%,
15% {
opacity: 1;
animation-timing-function: var(--ease-out);
}
21%,
100% {
opacity: 0;
}
}
@keyframes list-cycle {
0% {
opacity: 0;
}
4%,
88% {
opacity: 1;
}
96%,
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.list,
.r1,
.r2,
.r1 .card {
animation: none;
opacity: 1;
transform: none;
}
/* Overshoot is the first thing to go under reduced motion — it is pure
vestibular decoration. The grab and release survive as a fade. */
.shadow {
animation: hold-airborne 3.2s ease infinite;
}
@keyframes hold-airborne {
0%,
10% {
opacity: 0;
}
22%,
52% {
opacity: 1;
}
68%,
100% {
opacity: 0;
}
}
.pointer {
animation: hold-press 3.2s ease infinite;
transform: none;
}
@keyframes hold-press {
0%,
6% {
opacity: 0;
}
18%,
44% {
opacity: 1;
}
58%,
100% {
opacity: 0;
}
}
}
</style>
</head>
<body>
<div
class="list"
role="img"
aria-label="A list row flicked into the next slot, overshooting and settling back"
>
<div class="row r1">
<div class="card">
<span class="shadow"></span>
<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.