A small ball rolls around the outside of a card, hugging every rounded corner without slipping. Generated keyframes keep its speed even, and its radius fits a whole number of turns per lap.
Also known as: rolling border, rolling border animation css, ball rolling around border.
<div class="bdr-rolling-ball-border"> <span class="bdr-rolling-ball-border-ball" aria-hidden="true"><span class="bdr-rolling-ball-border-spin"></span></span> <span class="bdr-rolling-ball-border-title">Rolling out v2.4</span> <span class="bdr-rolling-ball-border-text">12 of 31 regions</span> </div>
.bdr-rolling-ball-border {
--c: var(--bdr-color, #3b82f6);
font-size: var(--bdr-size, 16px);
position: relative;
display: grid;
place-items: center;
width: 15em;
height: 8.5em;
border-radius: 1em;
font-family: system-ui, sans-serif;
font-weight: 600;
--t: 8s;
place-content: center;
gap: 0.25em;
background: color-mix(in srgb, var(--c) 6%, transparent);
box-shadow: 0 0 0 0.1em color-mix(in srgb, var(--c) 28%, transparent);
}
.bdr-rolling-ball-border-ball {
position: absolute;
top: 0;
left: 0;
width: calc(0.609em * 2);
height: calc(0.609em * 2);
margin: -0.609em 0 0 -0.609em;
border-radius: 50%;
background: radial-gradient(circle at 34% 30%, color-mix(in srgb, var(--c) 30%, #fff), var(--c) 55%, color-mix(in srgb, var(--c) 70%, #000));
box-shadow: 0 0.12em 0.3em -0.05em color-mix(in srgb, var(--c) 60%, #000);
animation: bdr-rolling-ball-border-lap var(--t) linear infinite;
}
.bdr-rolling-ball-border-spin {
position: absolute;
inset: 0;
border-radius: 50%;
background:
radial-gradient(circle at 50% 18%, #fff 0 0.09em, transparent 0.1em),
radial-gradient(circle at 50% 82%, color-mix(in srgb, #fff 55%, transparent) 0 0.07em, transparent 0.08em);
animation: bdr-rolling-ball-border-spin var(--t) linear infinite;
}
.bdr-rolling-ball-border-title { font-size: 1em; }
.bdr-rolling-ball-border-text {
font-size: 0.75em;
font-weight: 450;
opacity: 0.6;
}
@keyframes bdr-rolling-ball-border-lap {
0% { transform: translate(1em, -0.709em); }
26.153% { transform: translate(14em, -0.709em); }
27.051% { transform: translate(14.442em, -0.651em); }
27.948% { transform: translate(14.854em, -0.48em); }
28.846% { transform: translate(15.208em, -0.208em); }
29.743% { transform: translate(15.48em, 0.146em); }
30.641% { transform: translate(15.651em, 0.558em); }
31.538% { transform: translate(15.709em, 1em); }
44.615% { transform: translate(15.709em, 7.5em); }
45.513% { transform: translate(15.651em, 7.942em); }
46.41% { transform: translate(15.48em, 8.354em); }
47.308% { transform: translate(15.208em, 8.708em); }
48.205% { transform: translate(14.854em, 8.98em); }
49.103% { transform: translate(14.442em, 9.151em); }
50% { transform: translate(14em, 9.209em); }
76.153% { transform: translate(1em, 9.209em); }
77.051% { transform: translate(0.558em, 9.151em); }
77.948% { transform: translate(0.146em, 8.98em); }
78.846% { transform: translate(-0.208em, 8.708em); }
79.743% { transform: translate(-0.48em, 8.354em); }
80.641% { transform: translate(-0.651em, 7.942em); }
81.538% { transform: translate(-0.709em, 7.5em); }
94.615% { transform: translate(-0.709em, 1em); }
95.513% { transform: translate(-0.651em, 0.558em); }
96.41% { transform: translate(-0.48em, 0.146em); }
97.308% { transform: translate(-0.208em, -0.208em); }
98.205% { transform: translate(0.146em, -0.48em); }
99.103% { transform: translate(0.558em, -0.651em); }
100% { transform: translate(1em, -0.709em); }
}
/* A whole number of turns per lap, so the spin lines up at the wrap. */
@keyframes bdr-rolling-ball-border-spin {
to { transform: rotate(4680deg); }
}
@media (prefers-reduced-motion: reduce) {
.bdr-rolling-ball-border-ball,
.bdr-rolling-ball-border-spin { animation-play-state: paused; }
}
@media (prefers-reduced-motion: reduce) {
.bdr-rolling-ball-border, .bdr-rolling-ball-border *, .bdr-rolling-ball-border::before, .bdr-rolling-ball-border::after, .bdr-rolling-ball-border *::before, .bdr-rolling-ball-border *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--bdr-color on the element or a parent.
Currently previewing #3b82f6.--bdr-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.