What is the difference between a CSS transition, an animation and a view transition?

A CSS transition animates a property between two states when it changes, on hover or when a class is toggled, while a CSS animation plays keyframes on its own. View transitions go further: the View Transitions API captures the old and new state of the page and animates between them, so a card can grow into its detail view.

How to choose, performance and accessibility
  • Use Crossfade Transition or Fade Through between content states, Circle Reveal or Curtain Open for dramatic section changes, Dark Mode for a theme toggle, and Popover Bloom or Dialog Curtain for menus and modals.
  • Keep most interface transitions between about 150 and 400 ms, animate transform and opacity to avoid layout shifts, and let browsers without the View Transitions API switch instantly.
  • Fade Through, Crossfade Transition, Dark Mode and several other transitions are free with their code; the rest come with Premium.
Page A
Page B

Fade Through

The page gradually fades to black then reveals the new page.

CSS Opacity Smooth
View source code
HTML
<div class="demo-preview">
  <div class="transition-preview" id="fadeThrough">
    <div class="transition-content old-content">Page A</div>
    <div class="transition-content new-content">Page B</div>
    <div class="transition-overlay fade-through-overlay"></div>
  </div>
  <button class="trigger-btn" onclick="triggerFadeThrough()">Trigger Transition</button>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --space-4: 1rem;
}

.transition-preview {
  position: relative;
  width: 100%;
  height: 200px;
  border-radius: 12px;
  overflow: hidden;
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}

.transition-content {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  font-weight: 600;
  color: white;
}

.transition-content.old-content {
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
}

.transition-content.new-content {
  background: linear-gradient(135deg, #06b6d4, #10b981);
  opacity: 0;
  visibility: hidden;
}

.trigger-btn {
  padding: 12px 24px;
  font-size: 0.9rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
  color: white;
  transition: all 0.3s ease;
  margin-top: var(--space-4);
}

.trigger-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4);
}

.trigger-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.transition-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 10;
}

.fade-through-overlay {
  background: #0a0a0f;
  opacity: 0;
}

.fade-through-overlay.animating {
  animation: fadeThrough 1.2s ease-in-out;
}

@keyframes fadeThrough {
  0% {
    opacity: 0;
  }

  40% {
    opacity: 1;
  }

  60% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}
JavaScript
(function() {
  // Generic transition trigger function
  function triggerTransition(containerId, overlayClass, duration = 1200) {
    const container = document.getElementById(containerId);
    if (!container) return;
    const overlay = container.querySelector(overlayClass);
    const oldContent = container.querySelector('.old-content');
    const newContent = container.querySelector('.new-content');
    const btn = container.parentElement ? container.parentElement.querySelector('.trigger-btn') : null;
    if (!overlay || !oldContent || !newContent) return;

    if (btn) btn.disabled = true;
    overlay.classList.add('animating');

    // Swap content at midpoint
    setTimeout(() => {
      oldContent.style.opacity = '0';
      oldContent.style.visibility = 'hidden';
      newContent.style.opacity = '1';
      newContent.style.visibility = 'visible';
    }, duration * 0.5);

    // Reset after animation
    setTimeout(() => {
      overlay.classList.remove('animating');
      if (btn) btn.disabled = false;

      // Reset content for next demo
      setTimeout(() => {
        oldContent.style.opacity = '1';
        oldContent.style.visibility = 'visible';
        newContent.style.opacity = '0';
        newContent.style.visibility = 'hidden';
      }, 500);
    }, duration);
  }

  // 1. Fade Through
  function triggerFadeThrough() {
    triggerTransition('fadeThrough', '.fade-through-overlay', 1200);
  }
  window.triggerFadeThrough = triggerFadeThrough;
})();

Slide Over

New content slides over the old from left to right.

CSS Transform Slide

Circle Reveal

A circle expands from the center to reveal the new page.

CSS Clip-path Circle

Curtain Open

Two panels open like theater curtains.

CSS Transform Theatrical

Diagonal Wipe

A diagonal line sweeps across the screen to reveal the new content.

CSS Clip-path Wipe

Pixelate Dissolve

The screen breaks into pixels that appear and disappear.

CSS Grid Retro

Flip Page

The page flips over like a book.

CSS 3D Perspective

Zoom Through

Zoom into the screen center then zoom out on the new page.

CSS Scale Zoom

Blinds Effect

Horizontal blinds close then open to reveal the new page.

CSS Transform Blinds

Morph Transition

A shape morphs and expands to cover the screen then shrinks back.

CSS Morph Shape
★
Etat A
Home page
♥
Etat B
Page profil
Click to toggle

Crossfade Transition

Smooth crossfade between two states. Simulates the native crossfade of the View Transitions API.

CSS Crossfade Premium
View source code
HTML
<div class="demo-preview" onclick="toggleDemo(this, '.crossfade-container')">
  <div class="crossfade-container">
    <div class="crossfade-state crossfade-state-a">
      <div class="crossfade-icon">&#9733;</div>
      <div>
        <div style="font-size: 1rem; font-weight: 800;">Etat A</div>
        <div style="font-size: 0.65rem; opacity: 0.7; margin-top: 2px;">Home page</div>
      </div>
    </div>
    <div class="crossfade-state crossfade-state-b">
      <div class="crossfade-icon">&#9829;</div>
      <div>
        <div style="font-size: 1rem; font-weight: 800;">Etat B</div>
        <div style="font-size: 0.65rem; opacity: 0.7; margin-top: 2px;">Page profil</div>
      </div>
    </div>
  </div>
  <span class="vt-hint">Click to toggle</span>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.vt-hint {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.65rem;
  color: rgba(255, 255, 255, 0.35);
  text-transform: uppercase;
  letter-spacing: 2px;
  pointer-events: none;
}

.crossfade-container {
  width: 280px;
  height: 180px;
  border-radius: 16px;
  position: relative;
  overflow: hidden;
}

.crossfade-state {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1.1rem;
  letter-spacing: 1px;
  transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 16px;
}

.crossfade-state-a {
  background: linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7);
  opacity: 1;
}

.crossfade-state-b {
  background: linear-gradient(135deg, #f43f5e, #ec4899, #d946ef);
  opacity: 0;
}

.crossfade-container.active .crossfade-state-a {
  opacity: 0;
}

.crossfade-container.active .crossfade-state-b {
  opacity: 1;
}

.crossfade-icon {
  width: 50px;
  height: 50px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.2);
  margin-right: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  backdrop-filter: blur(10px);
}
JavaScript
(function() {
  function toggleDemo(previewEl, selector) {
    var container = previewEl.querySelector(selector);
    if (container) container.classList.toggle('active');
  }
  window.toggleDemo = toggleDemo;
})();

Slide Transition

Content slides left while new content comes in from the right, like page-to-page navigation.

CSS Slide Navigation

Scale Morph

The element changes size, shape and color with an elastic animation curve.

CSS Morph Premium

Shared Element

A shared element moves from one position to another, mimicking the shared element transition of the API.

CSS Shared Premium

Fade & Slide Up

Content fades out upward and new content rises from the bottom. Ideal for profiles and cards.

CSS Fade Slide

Flip Transition

The card performs a 3D rotation to reveal different content on the back. Classic and elegant effect.

CSS 3D Flip

Zoom Transition

Zoom-in effect that enlarges the view to transition to a new detailed state. Perfect for galleries.

CSS Zoom Premium

Circular Reveal

New content reveals itself with an expanding circle via clip-path. Ideal for theme toggles.

CSS Clip-path Premium

Stagger List

List items exit one by one with staggered delays, then new items appear progressively.

CSS Stagger List

Page Turn

Simulates a 3D page turn effect to reveal the next page. Inspired by e-readers and digital books.

CSS 3D Premium

Dark Mode

Dark theme active

Light Mode

Light theme active

Circular Reveal

Theme toggle with circular reveal via clip-path: circle() from the top-right button.

CSS clip-path Toggle
View source code
HTML
<div class="demo-preview">
  <div class="th-circular-scene" id="thCircular">
    <div class="th-circular-dark">
      <button class="th-circular-toggle th-circular-toggle--dark" onclick="event.stopPropagation(); document.getElementById('thCircular').classList.toggle('active')">
        <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
      </button>
      <div class="th-circular-card">
        <h3>Dark Mode</h3>
        <p>Dark theme active</p>
        <div class="th-circular-card-bar"><div class="th-circular-card-bar-fill"></div></div>
      </div>
    </div>
    <div class="th-circular-light">
      <button class="th-circular-toggle th-circular-toggle--light" onclick="event.stopPropagation(); document.getElementById('thCircular').classList.toggle('active')">
        <svg viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
      </button>
      <div class="th-circular-card">
        <h3>Light Mode</h3>
        <p>Light theme active</p>
        <div class="th-circular-card-bar"><div class="th-circular-card-bar-fill"></div></div>
      </div>
    </div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.th-circular-scene {
  width: 300px;
  height: 220px;
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  cursor: pointer;
}

.th-circular-dark {
  background: #0f172a;
  z-index: 1;
}

.th-circular-light {
  background: #f8fafc;
  z-index: 2;
  clip-path: circle(0% at 85% 15%);
}

.th-circular-dark h3 {
  color: #fff;
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
}

.th-circular-dark p {
  color: rgba(255, 255, 255, 0.5);
  font-size: 0.75rem;
  margin: 0;
}

.th-circular-light h3 {
  color: #0f172a;
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
}

.th-circular-light p {
  color: #64748b;
  font-size: 0.75rem;
  margin: 0;
}

.th-circular-toggle {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  transition: all 0.3s ease;
}

.th-circular-toggle--dark {
  background: rgba(255, 255, 255, 0.1);
}

.th-circular-toggle--light {
  background: rgba(0, 0, 0, 0.08);
}

.th-circular-toggle svg {
  width: 18px;
  height: 18px;
  stroke-width: 2;
  fill: none;
}

.th-circular-toggle--dark svg {
  stroke: #fbbf24;
}

.th-circular-toggle--light svg {
  stroke: #6366f1;
}

.th-circular-card {
  width: 220px;
  padding: 16px;
  border-radius: 12px;
}

.th-circular-dark .th-circular-card {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.08);
}

.th-circular-light .th-circular-card {
  background: #fff;
  border: 1px solid #e2e8f0;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}

.th-circular-card-bar {
  height: 6px;
  border-radius: 3px;
  margin-top: 10px;
}

.th-circular-dark .th-circular-card-bar {
  background: rgba(255, 255, 255, 0.08);
}

.th-circular-light .th-circular-card-bar {
  background: #e2e8f0;
}

.th-circular-card-bar-fill {
  height: 100%;
  width: 65%;
  border-radius: 3px;
  background: linear-gradient(90deg, #8b5cf6, #a78bfa);
}
JavaScript
/* Pure CSS — no JavaScript required */

Smooth Fade

Theme transition via cross-fade opacity with sun/moon icon morphing.

CSS Fade Opacity

Slide Wipe

Horizontal wipe with clip-path: inset() and a luminous separation line crossing the screen.

CSS Wipe clip-path

Color Morph

Theme transition entirely driven by CSS custom properties with transition on each variable.

CSS Variables Morph

Icon Toggle

Theme button with rotational morphing between sun and moon icons. Combined rotation and scale.

CSS Icon Morph

Card Theme

Complete card with avatar, text and buttons where each element transitions between the two themes.

CSS Card Transition

Gradient Shift

Gradient background shifting from cool nighttime hues to warm golden tones in 0.8s.

CSS Gradient Ambient

Adaptive UI

Miniature interface (nav, content, blocks) that toggles between dark and light theme on click.

CSS UI Adaptive

Split Compare

Side-by-side comparison of both themes with a luminous central separator and "VS" label.

CSS Compare Split

Pixelate Transition

Pixelated transition: colored blocks appear in random cascade to reveal the new theme.

CSS JS Pixel
Bloom Popover Content

Popover Bloom

The element blooms open from its anchor point with scale + opacity via @starting-style.

CSS @starting-style Popover
View source code
HTML
<div class="demo-preview">
  <div class="popover-bloom-anchor" id="popoverBloomAnchor">
    <div class="popover-bloom-box" id="popoverBloomBox">Bloom Popover Content</div>
    <button class="toggle-btn" onclick="togglePopoverBloom()">Toggle Popover</button>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.toggle-btn {
  padding: 10px 24px;
  font-size: 0.85rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  background: #6366f1;
  color: white;
  transition: background 0.2s, transform 0.1s;
}

.toggle-btn:hover {
  background: #4f46e5;
}

.toggle-btn:active {
  transform: scale(0.96);
}

.popover-bloom-anchor {
  position: relative;
  display: inline-block;
}

.popover-bloom-box {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
  color: white;
  padding: 14px 22px;
  border-radius: 12px;
  font-size: 0.85rem;
  font-weight: 500;
  white-space: nowrap;
  box-shadow: 0 10px 40px rgba(99, 102, 241, 0.4);
  opacity: 0;
  scale: 0.6;
  transition: opacity 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
    scale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
  transform-origin: bottom center;
}

.popover-bloom-box::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 8px solid transparent;
  border-top-color: #8b5cf6;
}

.popover-bloom-box.bloom-visible {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}
JavaScript
(function() {
  var popoverBloomOpen = false;

  function togglePopoverBloom() {
    var box = document.getElementById('popoverBloomBox');
    if (!box) return;
    popoverBloomOpen = !popoverBloomOpen;
    if (popoverBloomOpen) {
      box.classList.add('bloom-visible');
    } else {
      box.classList.remove('bloom-visible');
    }
  }
  window.togglePopoverBloom = togglePopoverBloom;
})();
Stage Curtain

Dialog Curtain

Modal that opens like stage curtains, separating from the center.

CSS Dialog Transition
View source code
HTML
<div class="demo-preview">
  <div class="curtain-preview-container" id="curtainPreview">
    <div class="curtain-preview-left"></div>
    <div class="curtain-preview-right"></div>
    <span class="curtain-preview-label">Stage Curtain</span>
  </div>
  <button class="toggle-btn" onclick="toggleCurtainDialog()">Open Curtain</button>
  <div class="curtain-dialog" id="curtainDialog">
    <div class="curtain-backdrop" onclick="closeCurtainDialog()"></div>
    <div class="curtain-left"></div>
    <div class="curtain-right"></div>
    <div class="curtain-content">
      <h3>Stage Curtain Dialog</h3>
      <p>Content appears after the curtains open.</p>
      <button class="curtain-close-btn" onclick="closeCurtainDialog()">Close</button>
    </div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.toggle-btn {
  padding: 10px 24px;
  font-size: 0.85rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  background: #6366f1;
  color: white;
  transition: background 0.2s, transform 0.1s;
}

.toggle-btn:hover {
  background: #4f46e5;
}

.toggle-btn:active {
  transform: scale(0.96);
}

.curtain-preview-container {
  position: relative;
  width: 200px;
  height: 120px;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: #0a0a1a;
}

.curtain-preview-left,
.curtain-preview-right {
  position: absolute;
  top: 0;
  width: 50%;
  height: 100%;
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

.curtain-preview-left {
  left: 0;
  background: linear-gradient(135deg, #312e81, #4338ca);
  transform: translateX(-100%);
}

.curtain-preview-right {
  right: 0;
  background: linear-gradient(225deg, #312e81, #4338ca);
  transform: translateX(100%);
}

.curtain-preview-container.curtain-active .curtain-preview-left {
  transform: translateX(0);
}

.curtain-preview-container.curtain-active .curtain-preview-right {
  transform: translateX(0);
}

.curtain-preview-label {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.8);
  opacity: 0;
  color: white;
  font-weight: 600;
  font-size: 0.75rem;
  z-index: 2;
  transition: opacity 0.3s ease 0.4s, transform 0.3s ease 0.4s;
  white-space: nowrap;
}

.curtain-preview-container.curtain-active .curtain-preview-label {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.curtain-left,
.curtain-right {
  width: 50%;
  height: 100%;
  background: linear-gradient(135deg, #1a1a2e, #0f172a);
}

.curtain-left {
  transform-origin: left center;
}

.curtain-right {
  transform-origin: right center;
}

.curtain-dialog {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: transparent;
  border: none;
  padding: 0;
  max-width: 100%;
  max-height: 100%;
  z-index: 1000;
  pointer-events: none;
  display: none;
}

.curtain-dialog.curtain-open {
  display: flex;
  pointer-events: auto;
}

.curtain-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0);
  transition: background 0.6s ease;
}

.curtain-dialog.curtain-open .curtain-backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.curtain-dialog .curtain-left,
.curtain-dialog .curtain-right {
  position: absolute;
  top: 0;
  width: 50%;
  height: 100%;
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

.curtain-dialog .curtain-left {
  left: 0;
  background: linear-gradient(135deg, #1e1b4b, #312e81);
  transform: translateX(-100%);
}

.curtain-dialog .curtain-right {
  right: 0;
  background: linear-gradient(225deg, #1e1b4b, #312e81);
  transform: translateX(100%);
}

.curtain-dialog.curtain-open .curtain-left {
  transform: translateX(0);
}

.curtain-dialog.curtain-open .curtain-right {
  transform: translateX(0);
}

.curtain-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.8);
  opacity: 0;
  background: linear-gradient(135deg, #1e1e2e, #2d2b55);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 20px;
  padding: 32px;
  color: white;
  text-align: center;
  z-index: 2;
  transition: opacity 0.4s ease 0.5s, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.5s;
  min-width: 200px;
}

.curtain-dialog.curtain-open .curtain-content {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.curtain-dialog.curtain-closing .curtain-content {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.8);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.curtain-dialog.curtain-closing .curtain-left {
  transform: translateX(-100%);
  transition-delay: 0.15s;
}

.curtain-dialog.curtain-closing .curtain-right {
  transform: translateX(100%);
  transition-delay: 0.15s;
}

.curtain-dialog.curtain-closing .curtain-backdrop {
  background: rgba(0, 0, 0, 0);
  transition-delay: 0.15s;
}

.curtain-content h3 {
  font-size: 1.2rem;
  margin: 0 0 8px;
}

.curtain-content p {
  margin: 0 0 16px;
  opacity: 0.7;
  font-size: 0.85rem;
}

.curtain-close-btn {
  padding: 8px 20px;
  background: #6366f1;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  font-size: 0.85rem;
}
JavaScript
(function() {
  var curtainOpen = false;

  function closeCurtainDialog() {
    var dialog = document.getElementById('curtainDialog');
    var preview = document.getElementById('curtainPreview');
    if (!dialog || !preview) return;
    curtainOpen = false;
    dialog.classList.add('curtain-closing');
    preview.classList.remove('curtain-active');
    setTimeout(function() {
      dialog.classList.remove('curtain-open', 'curtain-closing');
    }, 700);
  }

  function toggleCurtainDialog() {
    var dialog = document.getElementById('curtainDialog');
    var preview = document.getElementById('curtainPreview');
    if (!dialog || !preview) return;
    if (curtainOpen) {
      closeCurtainDialog();
      return;
    }
    curtainOpen = true;
    dialog.classList.remove('curtain-closing');
    dialog.classList.add('curtain-open');
    preview.classList.add('curtain-active');
  }

  function toggleCurtainDialog() {
    var dialog = document.getElementById('curtainDialog');
    var preview = document.getElementById('curtainPreview');
    if (!dialog || !preview) return;
    if (curtainOpen) {
      closeCurtainDialog();
      return;
    }
    curtainOpen = true;
    dialog.classList.remove('curtain-closing');
    dialog.classList.add('curtain-open');
    preview.classList.add('curtain-active');
  }

  function closeCurtainDialog() {
    var dialog = document.getElementById('curtainDialog');
    var preview = document.getElementById('curtainPreview');
    if (!dialog || !preview) return;
    curtainOpen = false;
    dialog.classList.add('curtain-closing');
    preview.classList.remove('curtain-active');
    setTimeout(function() {
      dialog.classList.remove('curtain-open', 'curtain-closing');
    }, 700);
  }
  window.closeCurtainDialog = closeCurtainDialog;
  window.toggleCurtainDialog = toggleCurtainDialog;
})();

Stage Curtain Dialog

Content appears after the curtains open.

Toast Cascade

Stacked toast notifications with fluid entry, reordering and graceful exit.

JS Animation Stack

Card Vanish

The card dissolves into particles on removal and materializes from particles on entry.

JS Particles Canvas

List Item Slide

Elements slide in from the side with staggered delays and collapse on removal.

CSS Stagger Collapse

Accordion Fold

Content sections that fold/unfold like paper with 3D perspective transforms.

CSS 3D Perspective

Morph Transition

The element morphs shape/position transitioning from display:none to display:block with allow-discrete.

CSS allow-discrete Morph

Portal Open

Content appears through an expanding circular mask, like a portal opening.

CSS clip-path Circle

Flip Card Entry

Elements pivot from the back face with 3D rotation to reveal content.

CSS 3D Rotation Backface

Fade Through Color

Elements pass through a brand color (not just opacity) during entry/exit.

CSS Color Transition

Motion Design

Staggered Reveal

Elements appearing in cascade with progressive delay for a coordinated effect.

CSS JS Cascade
View source code
HTML
<div class="demo-preview">
  <div class="md-stagger" id="mdStagger"></div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.md-stagger {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 20px;
  flex-wrap: wrap;
}

.md-stagger-item {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  background: linear-gradient(135deg, #6366f1, #a855f7);
  opacity: 0;
  transform: translateY(20px) scale(0.8);
  animation: mdStaggerIn 0.6s forwards;
}

@keyframes mdStaggerIn {
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}
JavaScript
(function() {
  // ============================================
  // Motion Design Effects
  // ============================================

  // 1. Staggered Reveal - generate items
  (function() {
    var container = document.getElementById('mdStagger');
    if (!container) return;
    var intervalId = null;

    function reset() {
      container.innerHTML = '';
      for (var i = 0; i < 12; i++) {
        var item = document.createElement('div');
        item.className = 'md-stagger-item';
        item.style.animationDelay = (i * 0.1) + 's';
        container.appendChild(item);
      }
    }
    reset();

    // Only loop when visible in viewport
    if ('IntersectionObserver' in window) {
      var obs = new IntersectionObserver(function(entries) {
        if (entries[0].isIntersecting) {
          if (!intervalId) intervalId = setInterval(reset, 3000);
        } else {
          if (intervalId) {
            clearInterval(intervalId);
            intervalId = null;
          }
        }
      }, {
        threshold: 0
      });
      obs.observe(container);
    } else {
      // Fallback: always loop
      intervalId = setInterval(reset, 3000);
    }
  })();
})();

Elastic Spring

Spring animation with physical bounce and realistic elastic deformation.

CSS Physics Bounce
View source code
HTML
<div class="demo-preview">
  <div class="md-spring">
    <div class="md-spring-ball"></div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.md-spring {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.md-spring-ball {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: linear-gradient(135deg, #6366f1, #ec4899);
  animation: mdSpring 2s cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}

@keyframes mdSpring {

  0%,
  100% {
    transform: translateY(0) scaleX(1) scaleY(1);
  }

  20% {
    transform: translateY(-60px) scaleX(0.9) scaleY(1.1);
  }

  40% {
    transform: translateY(0) scaleX(1.15) scaleY(0.85);
  }

  55% {
    transform: translateY(-25px) scaleX(0.95) scaleY(1.05);
  }

  70% {
    transform: translateY(0) scaleX(1.05) scaleY(0.95);
  }

  85% {
    transform: translateY(-8px);
  }
}
JavaScript
/* Pure CSS — no JavaScript required */

Orchestrated Motion

Coordinated multi-element animation After Effects-style with synchronized timing.

CSS JS Orchestrated

Kinetic Wave

Wave propagation through a dot grid with progressive delay.

CSS JS Wave

Inertia Momentum

Movement with inertia and natural deceleration: drag the cube and watch it slow down.

JS Physics Drag
View source code
HTML
<div class="demo-preview">
  <div class="md-inertia" id="mdInertia">
    <div class="md-inertia-box" id="mdInertiaBox"></div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.md-inertia {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.md-inertia-box {
  width: 50px;
  height: 50px;
  border-radius: 10px;
  background: linear-gradient(135deg, #6366f1, #06b6d4);
  cursor: grab;
  position: absolute;
  transition: none;
}
JavaScript
(function() {
  // 5. Inertia Momentum - drag with physics
  (function() {
    var container = document.getElementById('mdInertia');
    var box = document.getElementById('mdInertiaBox');
    if (!container || !box) return;

    var x = 0,
      y = 0,
      vx = 0,
      vy = 0;
    var isDragging = false,
      lastX = 0,
      lastY = 0;
    var friction = 0.95;
    var rect = container.getBoundingClientRect();

    box.style.left = (rect.width / 2 - 25) + 'px';
    box.style.top = (rect.height / 2 - 25) + 'px';

    box.addEventListener('mousedown', function(e) {
      isDragging = true;
      lastX = e.clientX;
      lastY = e.clientY;
      vx = 0;
      vy = 0;
      box.style.cursor = 'grabbing';
      e.preventDefault();
    });

    document.addEventListener('mousemove', function(e) {
      if (!isDragging) return;
      var dx = e.clientX - lastX;
      var dy = e.clientY - lastY;
      vx = dx;
      vy = dy;
      var l = parseFloat(box.style.left) + dx;
      var t = parseFloat(box.style.top) + dy;
      box.style.left = l + 'px';
      box.style.top = t + 'px';
      lastX = e.clientX;
      lastY = e.clientY;
    });

    document.addEventListener('mouseup', function() {
      if (isDragging) {
        isDragging = false;
        box.style.cursor = 'grab';
        animate();
      }
    });

    function animate() {
      if (isDragging) return;
      if (Math.abs(vx) < 0.1 && Math.abs(vy) < 0.1) return;
      vx *= friction;
      vy *= friction;
      var l = parseFloat(box.style.left) + vx;
      var t = parseFloat(box.style.top) + vy;
      var cRect = container.getBoundingClientRect();
      if (l < 0 || l > cRect.width - 50) vx *= -0.5;
      if (t < 0 || t > cRect.height - 50) vy *= -0.5;
      l = Math.max(0, Math.min(cRect.width - 50, l));
      t = Math.max(0, Math.min(cRect.height - 50, t));
      box.style.left = l + 'px';
      box.style.top = t + 'px';
      requestAnimationFrame(animate);
    }
  })();
})();

SVG Path Follow

Element following a complex SVG path with a fluid animation along the curve.

SVG JS Path

Character Cascade

Text with character-by-character animation, each letter appears individually.

JS Text Cascade
View source code
HTML
<div class="demo-preview">
  <div class="md-char-cascade" id="mdCharCascade"></div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.md-char-cascade {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.md-char {
  display: inline-block;
  font-size: 2rem;
  font-weight: 700;
  color: #fff;
  opacity: 0;
  transform: translateY(20px);
  animation: mdCharIn 0.4s forwards;
}

@keyframes mdCharIn {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
JavaScript
(function() {
  // 7. Character Cascade
  (function() {
    var container = document.getElementById('mdCharCascade');
    if (!container) return;
    var words = ['MOTION', 'DESIGN', 'EFFECT', 'SMOOTH'];
    var currentWord = 0;
    var intervalId = null;

    function showWord() {
      container.innerHTML = '';
      var word = words[currentWord];
      word.split('').forEach(function(char, i) {
        var span = document.createElement('span');
        span.className = 'md-char';
        span.textContent = char;
        span.style.animationDelay = (i * 0.08) + 's';
        container.appendChild(span);
      });
      currentWord = (currentWord + 1) % words.length;
    }
    showWord();

    // Only loop when visible in viewport
    if ('IntersectionObserver' in window) {
      var obs = new IntersectionObserver(function(entries) {
        if (entries[0].isIntersecting) {
          if (!intervalId) intervalId = setInterval(showWord, 2500);
        } else {
          if (intervalId) {
            clearInterval(intervalId);
            intervalId = null;
          }
        }
      }, {
        threshold: 0
      });
      obs.observe(container);
    } else {
      // Fallback: always loop
      intervalId = setInterval(showWord, 2500);
    }
  })();
})();

3D Flip Sequence

Chained 3D flip sequence with delay between each card for a domino effect.

CSS 3D Flip Sequence

Shape Morph Transition

Fluid transition between geometric shapes: circle, organic blob, square, and back.

CSS Morph Shapes

Timeline Choreography

Multi-step synchronized timeline sequence with animated progress bars.

CSS JS Timeline

View Transitions Page Reveal

Smooth morph between a grid and its detail view using the browser's native View Transitions API. Automatic fallback if unsupported.

View TransitionsNative APIPremium

Cross-Document Page Morph

Smooth transition between two real pages (multi-page): the shared element morphs from one page to the other via the native View Transitions API, without a single line of JavaScript. The SPA feel, without the SPA cost.

💡 Perfect for: blog list→article, e-commerce listing→product page, portfolio→case study, docs, any multi-page site.

View Transitions MPA 0 JS Premium

Frequently Asked Questions

What is the difference between transition and animation in CSS? +

A transition animates the change from state A to state B (e.g.: hover). An animation can have multiple steps (@keyframes), loop, and trigger without interaction. Use transition for interactions, animation for autonomous effects.

How to create a page transition in CSS? +

Use the View Transitions API (document.startViewTransition()) for page transitions. For within-page transitions, combine opacity and transform with a toggleable class. Customize curves with cubic-bezier.

What is the ideal duration for a CSS transition? +

Between 200ms and 400ms for interactions (hover, click). 300ms is the standard. Below 150ms, the animation is too fast to be perceived. Above 500ms, it feels slow. Use ease-out for entrances and ease-in for exits.

How to avoid layout shift during a transition? +

Only animate transform and opacity (not width, height, margin). Use position:absolute to take the element out of the flow. Reserve space with min-height. Prefer scale() over width for enlargements.

How to chain multiple CSS transitions? +

Use transition-delay on each property: transition: opacity 0.3s ease 0s, transform 0.3s ease 0.1s. Or use @keyframes with percentages for full control over the sequence.

Similar Categories