Pure CSSMIT licensed

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.

Live Demo
Try it

Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS

ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link

10% off at checkout with code
Check ZTM Course

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>
.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;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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] AND animate-[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 top or left — every frame triggers layout+paint. Use transform: translate which 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

ChromeSafariFirefoxEdge
45+10+40+45+

transform + @keyframes + animation-direction. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…