16 CSS Bounce Animations15 / 16
DVD Screensaver Viewport Bounce
The cult DVD screensaver bounce — a logo travels diagonally across the viewport and cleanly bounces off all four edges, hoping to land perfectly in a corner (the internet's favorite unsolved problem). Retro dark surface with an 80s neon-purple logo. Pure CSS with separate X and Y keyframes at coprime durations so the trajectory never visually repeats. Zero JavaScript. Every 90s kid searches for this at some point.
Published
The code
<div class="bn-15">
<p class="bn-15__label">DVD screensaver viewport bounce</p>
<div class="bn-15__viewport" aria-hidden="true">
<div class="bn-15__logo">DVD</div>
</div>
<p class="bn-15__hint">X + Y keyframes at coprime durations — trajectory never visually repeats.</p>
</div><div class="bn-15">
<p class="bn-15__label">DVD screensaver viewport bounce</p>
<div class="bn-15__viewport" aria-hidden="true">
<div class="bn-15__logo">DVD</div>
</div>
<p class="bn-15__hint">X + Y keyframes at coprime durations — trajectory never visually repeats.</p>
</div>.bn-15 {
--page-bg: #050510;
--viewport-bg: #0a0a18;
--ink: #f5f5fa;
--sub: #8b8ba0;
--neon-a: #a855f7;
--neon-b: #22d3ee;
--line: #1a1a30;
font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 32px;
background: radial-gradient(80% 60% at 50% 50%, rgba(168,85,247,.08) 0%, transparent 60%), var(--page-bg);
color: var(--ink);
text-align: center;
}
.bn-15 *,
.bn-15 *::before,
.bn-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-15 ::selection {
background: var(--neon-a);
color: #050510;
}
.bn-15__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-15__viewport {
position: relative;
width: min(600px,100%);
aspect-ratio: 16/9;
background: var(--viewport-bg);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 20px 40px -12px rgba(168,85,247,.14),inset 0 0 60px rgba(168,85,247,.08);
}
.bn-15__logo {
position: absolute;
top: 0;
left: 0;
padding: 10px 22px;
border-radius: 10px;
background: linear-gradient(135deg,var(--neon-a),var(--neon-b));
color: #fff;
font-family: 'JetBrains Mono',ui-monospace,monospace;
font-size: 1.1rem;
font-weight: 900;
letter-spacing: .08em;
box-shadow: 0 0 20px rgba(168,85,247,.5),0 0 40px rgba(34,211,238,.35);
animation: bn-15-x 4.7s linear infinite alternate, bn-15-y 3.3s linear infinite alternate;
will-change: transform;
}
@keyframes bn-15-x {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(600px - 100%));
}
}
@keyframes bn-15-y {
0% {
transform: translateY(0);
}
100% {
transform: translateY(calc(340px - 100%));
}
}
/* Combine both transforms via the CSS animation composition — modern browsers stack */
@supports (animation-composition: add) {
.bn-15__logo {
animation-composition: add;
}
}
/* Fallback for browsers without composition: single combined keyframe (won't produce independent X/Y coprime bounce but stays visually acceptable) */
@supports not (animation-composition: add) {
.bn-15__logo {
animation: bn-15-combined 4.7s linear infinite alternate;
}
@keyframes bn-15-combined {
0% {
transform: translate(0,0);
}
25% {
transform: translate(calc(600px - 100%),40px);
}
50% {
transform: translate(calc(600px - 100%),calc(340px - 100%));
}
75% {
transform: translate(0,calc(340px - 100%));
}
100% {
transform: translate(0,0);
}
}
}
.bn-15__hint {
font-size: .78rem;
color: var(--sub);
max-width: 400px;
line-height: 1.5;
}
@media (prefers-reduced-motion: reduce) {
.bn-15__logo {
animation: none!important;
transform: translate(calc((600px - 100%) / 2),calc((340px - 100%) / 2))!important;
}
}.bn-15 {
--page-bg: #050510;
--viewport-bg: #0a0a18;
--ink: #f5f5fa;
--sub: #8b8ba0;
--neon-a: #a855f7;
--neon-b: #22d3ee;
--line: #1a1a30;
font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 32px;
background: radial-gradient(80% 60% at 50% 50%, rgba(168,85,247,.08) 0%, transparent 60%), var(--page-bg);
color: var(--ink);
text-align: center;
}
.bn-15 *,
.bn-15 *::before,
.bn-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-15 ::selection {
background: var(--neon-a);
color: #050510;
}
.bn-15__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-15__viewport {
position: relative;
width: min(600px,100%);
aspect-ratio: 16/9;
background: var(--viewport-bg);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 20px 40px -12px rgba(168,85,247,.14),inset 0 0 60px rgba(168,85,247,.08);
}
.bn-15__logo {
position: absolute;
top: 0;
left: 0;
padding: 10px 22px;
border-radius: 10px;
background: linear-gradient(135deg,var(--neon-a),var(--neon-b));
color: #fff;
font-family: 'JetBrains Mono',ui-monospace,monospace;
font-size: 1.1rem;
font-weight: 900;
letter-spacing: .08em;
box-shadow: 0 0 20px rgba(168,85,247,.5),0 0 40px rgba(34,211,238,.35);
animation: bn-15-x 4.7s linear infinite alternate, bn-15-y 3.3s linear infinite alternate;
will-change: transform;
}
@keyframes bn-15-x {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(600px - 100%));
}
}
@keyframes bn-15-y {
0% {
transform: translateY(0);
}
100% {
transform: translateY(calc(340px - 100%));
}
}
/* Combine both transforms via the CSS animation composition — modern browsers stack */
@supports (animation-composition: add) {
.bn-15__logo {
animation-composition: add;
}
}
/* Fallback for browsers without composition: single combined keyframe (won't produce independent X/Y coprime bounce but stays visually acceptable) */
@supports not (animation-composition: add) {
.bn-15__logo {
animation: bn-15-combined 4.7s linear infinite alternate;
}
@keyframes bn-15-combined {
0% {
transform: translate(0,0);
}
25% {
transform: translate(calc(600px - 100%),40px);
}
50% {
transform: translate(calc(600px - 100%),calc(340px - 100%));
}
75% {
transform: translate(0,calc(340px - 100%));
}
100% {
transform: translate(0,0);
}
}
}
.bn-15__hint {
font-size: .78rem;
color: var(--sub);
max-width: 400px;
line-height: 1.5;
}
@media (prefers-reduced-motion: reduce) {
.bn-15__logo {
animation: none!important;
transform: translate(calc((600px - 100%) / 2),calc((340px - 100%) / 2))!important;
}
}Here's a working CSS Bounce Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: DVD Screensaver Viewport Bounce
Source: https://codefronts.com/motion/css-bounce-animation/dvd-screensaver-viewport-bounce/
The cult DVD screensaver bounce — a logo travels diagonally across the viewport and cleanly bounces off all four edges, hoping to land perfectly in a corner (the internet's favorite unsolved problem). Retro dark surface with an 80s neon-purple logo. Pure CSS with separate X and Y keyframes at coprime durations so the trajectory never visually repeats. Zero JavaScript. Every 90s kid searches for this at some point.
## HTML
```html
<div class="bn-15">
<p class="bn-15__label">DVD screensaver viewport bounce</p>
<div class="bn-15__viewport" aria-hidden="true">
<div class="bn-15__logo">DVD</div>
</div>
<p class="bn-15__hint">X + Y keyframes at coprime durations — trajectory never visually repeats.</p>
</div>
```
## CSS
```css
.bn-15 {
--page-bg: #050510;
--viewport-bg: #0a0a18;
--ink: #f5f5fa;
--sub: #8b8ba0;
--neon-a: #a855f7;
--neon-b: #22d3ee;
--line: #1a1a30;
font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 32px;
background: radial-gradient(80% 60% at 50% 50%, rgba(168,85,247,.08) 0%, transparent 60%), var(--page-bg);
color: var(--ink);
text-align: center;
}
.bn-15 *,
.bn-15 *::before,
.bn-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-15 ::selection {
background: var(--neon-a);
color: #050510;
}
.bn-15__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-15__viewport {
position: relative;
width: min(600px,100%);
aspect-ratio: 16/9;
background: var(--viewport-bg);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 20px 40px -12px rgba(168,85,247,.14),inset 0 0 60px rgba(168,85,247,.08);
}
.bn-15__logo {
position: absolute;
top: 0;
left: 0;
padding: 10px 22px;
border-radius: 10px;
background: linear-gradient(135deg,var(--neon-a),var(--neon-b));
color: #fff;
font-family: 'JetBrains Mono',ui-monospace,monospace;
font-size: 1.1rem;
font-weight: 900;
letter-spacing: .08em;
box-shadow: 0 0 20px rgba(168,85,247,.5),0 0 40px rgba(34,211,238,.35);
animation: bn-15-x 4.7s linear infinite alternate, bn-15-y 3.3s linear infinite alternate;
will-change: transform;
}
@keyframes bn-15-x {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(600px - 100%));
}
}
@keyframes bn-15-y {
0% {
transform: translateY(0);
}
100% {
transform: translateY(calc(340px - 100%));
}
}
/* Combine both transforms via the CSS animation composition — modern browsers stack */
@supports (animation-composition: add) {
.bn-15__logo {
animation-composition: add;
}
}
/* Fallback for browsers without composition: single combined keyframe (won't produce independent X/Y coprime bounce but stays visually acceptable) */
@supports not (animation-composition: add) {
.bn-15__logo {
animation: bn-15-combined 4.7s linear infinite alternate;
}
@keyframes bn-15-combined {
0% {
transform: translate(0,0);
}
25% {
transform: translate(calc(600px - 100%),40px);
}
50% {
transform: translate(calc(600px - 100%),calc(340px - 100%));
}
75% {
transform: translate(0,calc(340px - 100%));
}
100% {
transform: translate(0,0);
}
}
}
.bn-15__hint {
font-size: .78rem;
color: var(--sub);
max-width: 400px;
line-height: 1.5;
}
@media (prefers-reduced-motion: reduce) {
.bn-15__logo {
animation: none!important;
transform: translate(calc((600px - 100%) / 2),calc((340px - 100%) / 2))!important;
}
}
```Here's a working CSS Bounce Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: DVD Screensaver Viewport Bounce
Source: https://codefronts.com/motion/css-bounce-animation/dvd-screensaver-viewport-bounce/
The cult DVD screensaver bounce — a logo travels diagonally across the viewport and cleanly bounces off all four edges, hoping to land perfectly in a corner (the internet's favorite unsolved problem). Retro dark surface with an 80s neon-purple logo. Pure CSS with separate X and Y keyframes at coprime durations so the trajectory never visually repeats. Zero JavaScript. Every 90s kid searches for this at some point.
## HTML
```html
<div class="bn-15">
<p class="bn-15__label">DVD screensaver viewport bounce</p>
<div class="bn-15__viewport" aria-hidden="true">
<div class="bn-15__logo">DVD</div>
</div>
<p class="bn-15__hint">X + Y keyframes at coprime durations — trajectory never visually repeats.</p>
</div>
```
## CSS
```css
.bn-15 {
--page-bg: #050510;
--viewport-bg: #0a0a18;
--ink: #f5f5fa;
--sub: #8b8ba0;
--neon-a: #a855f7;
--neon-b: #22d3ee;
--line: #1a1a30;
font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 32px;
background: radial-gradient(80% 60% at 50% 50%, rgba(168,85,247,.08) 0%, transparent 60%), var(--page-bg);
color: var(--ink);
text-align: center;
}
.bn-15 *,
.bn-15 *::before,
.bn-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-15 ::selection {
background: var(--neon-a);
color: #050510;
}
.bn-15__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-15__viewport {
position: relative;
width: min(600px,100%);
aspect-ratio: 16/9;
background: var(--viewport-bg);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 20px 40px -12px rgba(168,85,247,.14),inset 0 0 60px rgba(168,85,247,.08);
}
.bn-15__logo {
position: absolute;
top: 0;
left: 0;
padding: 10px 22px;
border-radius: 10px;
background: linear-gradient(135deg,var(--neon-a),var(--neon-b));
color: #fff;
font-family: 'JetBrains Mono',ui-monospace,monospace;
font-size: 1.1rem;
font-weight: 900;
letter-spacing: .08em;
box-shadow: 0 0 20px rgba(168,85,247,.5),0 0 40px rgba(34,211,238,.35);
animation: bn-15-x 4.7s linear infinite alternate, bn-15-y 3.3s linear infinite alternate;
will-change: transform;
}
@keyframes bn-15-x {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(600px - 100%));
}
}
@keyframes bn-15-y {
0% {
transform: translateY(0);
}
100% {
transform: translateY(calc(340px - 100%));
}
}
/* Combine both transforms via the CSS animation composition — modern browsers stack */
@supports (animation-composition: add) {
.bn-15__logo {
animation-composition: add;
}
}
/* Fallback for browsers without composition: single combined keyframe (won't produce independent X/Y coprime bounce but stays visually acceptable) */
@supports not (animation-composition: add) {
.bn-15__logo {
animation: bn-15-combined 4.7s linear infinite alternate;
}
@keyframes bn-15-combined {
0% {
transform: translate(0,0);
}
25% {
transform: translate(calc(600px - 100%),40px);
}
50% {
transform: translate(calc(600px - 100%),calc(340px - 100%));
}
75% {
transform: translate(0,calc(340px - 100%));
}
100% {
transform: translate(0,0);
}
}
}
.bn-15__hint {
font-size: .78rem;
color: var(--sub);
max-width: 400px;
line-height: 1.5;
}
@media (prefers-reduced-motion: reduce) {
.bn-15__logo {
animation: none!important;
transform: translate(calc((600px - 100%) / 2),calc((340px - 100%) / 2))!important;
}
}
```How this works
Two independent animations run simultaneously on the same logo element: @keyframes bn-15-x handles horizontal travel — 0% translateX(-100%) → 100% translateX(100%) with animation-direction: alternate so it ping-pongs between left and right edges. @keyframes bn-15-y handles vertical travel — same pattern for top and bottom. Both use animation-timing-function: linear (constant velocity — real DVD screensavers don't accelerate) and animation-iteration-count: infinite.
The key trick: X and Y use DIFFERENT durations. If both ran for 4 seconds, the logo would trace a perfect X pattern and return to start every 4 seconds — boring and predictable. With X at 4.7 seconds and Y at 3.3 seconds (coprime — no small common multiple), the trajectory takes many minutes to repeat visually. Users perceive the motion as "random" even though it's deterministic.
On each edge collision, CSS animation-direction: alternate reverses that axis — the logo doesn't bounce with a spring or squash; it snaps 180° at the edge (like the real DVD screensaver, which was just linear ping-pong not physics). The illusion of "bouncing" comes from the direction change, not from any elastic keyframe.
The demo shows a bordered viewport-style container (~600×340px) with a small logo pill inside currently mid-flight. On prefers-reduced-motion, the animation stops — logo rests at its center.
Make it yours
- Change the logo speed by editing the X and Y durations — faster (2s / 1.3s) reads as more energetic, slower (10s / 7s) reads as more meditative. Keep the two values coprime (no small common multiple).
- For a truly random-feeling trajectory, use non-coprime but irrational-ratio durations like 4.7s / 3.3s or 5.2s / 3.9s. Common multiples over 100 seconds make the loop visually infinite.
- For a corner-hit celebration (Reddit's r/DVDScreensaverPointsCorner obsession), add a click/hover-triggered highlight animation when the logo is near a corner — pure CSS can't detect collision, but you can flash the logo periodically to simulate the excitement.
- For platforms with dynamic viewport sizes, use viewport units (
translateX(calc(100vw - 100%))) instead of container-relative percentages. Requires re-thinking the container-relative CSS. - For Tailwind: register two keyframes and animation utilities. Use two class names on the logo —
animate-[bn15x_4.7s_linear_infinite_alternate]ANDanimate-[bn15y_3.3s_linear_infinite_alternate]. See FAQ #4.
Gotchas — read before shipping
- The X and Y keyframes MUST be applied to the SAME element for the diagonal motion to work — split them across parent and child and the transforms compose but centering breaks.
- The two animation durations MUST be coprime OR irrational-ratio — if both are simple ratios of the same base (e.g., 4s and 8s), the trajectory repeats visually every 8 seconds. Users perceive the repetition instantly.
- Do NOT animate
toporleft— every frame triggers layout+paint. Usetransform: translatewhich stays on the compositor thread. - For real corner-perfect bounces (the internet's obsession), you'd need to calculate viewport dimensions and use exact translate values — but even then, floating-point rounding usually prevents perfect corner hits. Perfectionism aside, the visual illusion is fine.
- For accessibility, this animation is PURELY DECORATIVE — no information conveyed. On
prefers-reduced-motion, disable entirely. Vestibular-disorder users find continuous diagonal motion nauseating. - WCAG 2.2.2 (Pause, Stop, Hide) — any animation over 5 seconds needs a pause mechanism. This runs infinitely, so add a pause button in production. The demo is auto-paused via
prefers-reduced-motion, which technically satisfies the rule for users who need it. - For nostalgic contexts (404 pages, error pages, empty states), the DVD screensaver bounce works. For serious contexts (checkout, medical forms), it's inappropriate — decorative motion feels frivolous.
- The logo's colors + font should match the 80s DVD aesthetic (neon purple / cyan on black, bold retro sans) for the visual reference to land. Modern gradient logos on a light background lose the nostalgic resonance.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 45+ | 10+ | 40+ | 45+ |
transform + @keyframes + animation-direction. Universal, GPU-accelerated.