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
Effect page →
View source code
HTML
<div class="ft-stage">
  <div class="transition-preview">
    <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 type="button" class="trigger-btn">Trigger Transition</button>
</div>
CSS
.ft-stage,
.ft-stage *,
.ft-stage *::before,
.ft-stage *::after {
  box-sizing: border-box
}

/* Racine de l'effet : écran en haut, bouton dessous. */
.ft-stage {
  --space-4: 1rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 300px;
  padding: 16px;
  position: relative;
  background: rgb(26, 26, 36);
  color: #fff;
  font-family: sans-serif
}

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

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

.ft-stage .transition-content.old-content {
  background: linear-gradient(135deg, #4f46e5, #7c3aed)
}

.ft-stage .transition-content.new-content {
  background: linear-gradient(135deg, #0e7490, #047857);
  opacity: 0;
  visibility: hidden
}

/* Page B affichée : la classe .is-b est posée sur l'écran au milieu de la transition, sous le calque,
   et retirée au clic suivant (retour à A). */
.ft-stage .transition-preview.is-b .old-content {
  opacity: 0;
  visibility: hidden
}

.ft-stage .transition-preview.is-b .new-content {
  opacity: 1;
  visibility: visible
}

.ft-stage .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, #4f46e5, #7c3aed);
  color: white;
  transition: all 0.3s ease;
  margin-top: var(--space-4)
}

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

.ft-stage .trigger-btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none
}

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

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

.ft-stage .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;
  }
}

/* Mouvement réduit : pas de calque, les deux pages s'échangent en fondu de 150 ms. */
@media (prefers-reduced-motion: reduce) {
  .ft-stage .transition-content {
    transition: opacity .15s, visibility .15s
  }
}
JavaScript
(function () {
  /* fx-0685 — Fade Through : un voile noir monte jusqu'à couvrir la page A, la page change dessous,
     puis le calque se retire sur la nouvelle page. Un clic mène à la page B et y reste ; le clic suivant ramène à A. */
  var SWAP_AT = 600;   // échange A ⇄ B, au milieu : le calque couvre tout l'écran
  var END_AT = 1200;   // fin de l'animation du calque (même durée que le CSS)
  var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.ft-stage').forEach(function (stage) {
    var screen = stage.querySelector('.transition-preview');
    var layer = stage.querySelector('.fade-through-overlay');
    var btn = stage.querySelector('.trigger-btn');
    if (!screen || !layer || !btn) return;
    var busy = false;

    btn.addEventListener('click', function () {
      if (busy) return;
      // Mouvement réduit : pas de calque, échange direct (fondu de 150 ms dans le CSS)
      if (reduceMotion.matches) {
        screen.classList.toggle('is-b');
        return;
      }
      busy = true;
      btn.setAttribute('aria-disabled', 'true'); // et non disabled, qui ferait perdre le focus clavier
      layer.classList.add('animating');
      setTimeout(function () {
        screen.classList.toggle('is-b');
      }, SWAP_AT);
      setTimeout(function () {
        layer.classList.remove('animating');
        btn.removeAttribute('aria-disabled');
        busy = false;
      }, END_AT);
    });
  });
})();

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 JS 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

Bubble Morph

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

CSS JS Morph Shape
State A
Home page
Click to toggle

Crossfade Transition

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

CSS Crossfade Premium
Effect page →
View source code
HTML
<div class="crossfade-stage">
  <div class="crossfade-frame" role="button" tabindex="0" aria-pressed="false">
    <div class="crossfade-state crossfade-state-a">
      <div class="crossfade-icon" aria-hidden="true">&#9733;</div>
      <div>
        <div style="font-size: 1rem; font-weight: 800;">State 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" aria-hidden="true">
      <div class="crossfade-icon" aria-hidden="true">&#9829;</div>
      <div>
        <div style="font-size: 1rem; font-weight: 800;">State B</div>
        <div style="font-size: 0.65rem; opacity: 0.7; margin-top: 2px;">Profile page</div>
      </div>
    </div>
  </div>
  <span class="crossfade-hint">Click to toggle</span>
</div>
CSS
.crossfade-stage,
.crossfade-stage *,
.crossfade-stage *::before,
.crossfade-stage *::after {
  box-sizing: border-box
}

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

.crossfade-hint {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  font-size: .65rem;
  color: rgba(255, 255, 255, .35);
  text-transform: uppercase;
  letter-spacing: 2px;
  white-space: nowrap;
  pointer-events: none
}

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

.crossfade-frame:focus-visible {
  outline: 2px solid #f9a8d4;
  outline-offset: 4px
}

.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-frame.active .crossfade-state-a {
  opacity: 0
}

.crossfade-frame.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)
}

/* Mouvement réduit : le fondu reste (aucun déplacement), raccourci à 0,15 s */
@media (prefers-reduced-motion: reduce) {
  .crossfade-state {
    transition-duration: .15s
  }
}
JavaScript
(function() {
  /* fx-0695 : Crossfade Transition — chaque .crossfade-frame de la page bascule entre l'état A et l'état B
     (clic, Entrée ou Espace) et reste sur l'état atteint ; l'action suivante fait le chemin inverse. */
  document.querySelectorAll('.crossfade-frame').forEach(function(frame) {
    var a = frame.querySelector('.crossfade-state-a');
    var b = frame.querySelector('.crossfade-state-b');
    function toggle() {
      var on = frame.classList.toggle('active');
      frame.setAttribute('aria-pressed', on ? 'true' : 'false');
      if (a) a.setAttribute('aria-hidden', on ? 'true' : 'false');
      if (b) b.setAttribute('aria-hidden', on ? 'false' : 'true');
    }
    frame.addEventListener('click', toggle);
    frame.addEventListener('keydown', function(e) {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
    });
  });
})();

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

On click, a circle grows from the center of the card (clip-path) and reveals the second state; a second click closes it. Ideal for theme toggles.

CSS JS Clip-path Premium

Stagger List

On click, list items slide out one by one with growing delays, then cascade back in on the second click. Works with your own items.

CSS JS 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

Dark Mode

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

CSS clip-path Toggle
Effect page →
View source code
HTML
<div class="th-circular-root">
  <div class="th-circular-scene">
    <button class="th-circular-toggle" type="button" aria-pressed="false" aria-label="Light theme">
      <svg class="th-circular-sun" viewBox="0 0 24 24" aria-hidden="true"><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>
      <svg class="th-circular-moon" viewBox="0 0 24 24" aria-hidden="true"><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-dark">
      <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" aria-hidden="true">
      <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
.th-circular-root,
.th-circular-root *,
.th-circular-root *::before,
.th-circular-root *::after {
  box-sizing: border-box
}

.th-circular-root {
  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;
}

/* Les deux thèmes sont deux calques superposés de la taille de la scène */
.th-circular-dark,
.th-circular-light {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

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

/* Le clair attend, réduit à un cercle de rayon 0 centré sur le bouton (30 px du bord haut et du bord droit) */
.th-circular-light {
  background: #f8fafc;
  z-index: 2;
  clip-path: circle(0% at calc(100% - 30px) 30px);
  transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

.th-circular-scene.active .th-circular-light {
  clip-path: circle(150% at calc(100% - 30px) 30px);
}

.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;
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  background: rgba(255, 255, 255, 0.1);
  transition: background-color 0.3s ease;
}

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

.th-circular-toggle:focus-visible {
  outline: 2px solid #a78bfa;
  outline-offset: 2px;
}

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

.th-circular-sun {
  stroke: #fbbf24;
}

.th-circular-moon {
  stroke: #6366f1;
  display: none;
}

.th-circular-scene.active .th-circular-sun {
  display: none;
}

.th-circular-scene.active .th-circular-moon {
  display: block;
}

.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);
}

/* Mouvement réduit : pas de cercle qui grandit, un fondu de 150 ms */
@media (prefers-reduced-motion: reduce) {
  .th-circular-light,
  .th-circular-scene.active .th-circular-light {
    clip-path: none;
  }
  .th-circular-light {
    opacity: 0;
    transition: opacity 0.15s linear;
  }
  .th-circular-scene.active .th-circular-light {
    opacity: 1;
  }
}
JavaScript
(function () {
  /* fx-0705 Dark Mode : le bouton de chaque .th-circular-scene pose ou retire .active (le CSS fait la transition) */
  document.querySelectorAll('.th-circular-scene').forEach(function (scene) {
    var btn = scene.querySelector('.th-circular-toggle');
    var dark = scene.querySelector('.th-circular-dark');
    var light = scene.querySelector('.th-circular-light');
    if (!btn) return;
    function sync() {
      var on = scene.classList.contains('active');
      btn.setAttribute('aria-pressed', on ? 'true' : 'false');
      if (dark) dark.setAttribute('aria-hidden', on ? 'true' : 'false');
      if (light) light.setAttribute('aria-hidden', on ? 'false' : 'true');
    }
    btn.addEventListener('click', function () {
      scene.classList.toggle('active');
      sync();
    });
    sync();
  });
})();

Smooth Fade

Light/dark theme transition via cross-fade opacity; the moon icon spins away as the sun takes its place.

CSS JS Fade Opacity

Wipe Transition

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

CSS Wipe clip-path

CSS Variables

Theme transition driven by six CSS custom properties: the colors that read them change smoothly.

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 switches between dark and light theme with the sun/moon button in its bar.

CSS JS UI Adaptive

Split Compare

Both themes stacked, with a "VS" divider you drag (mouse, touch, keyboard) to compare.

CSS JS Compare

Pixel Transition

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

CSS JS Pixel
The bubble blooms from its anchor

Popover Bloom

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

CSS @starting-style Popover
Effect page →
View source code
HTML
<div class="popover-bloom-stage">
  <button class="popover-bloom-btn" type="button" popovertarget="popoverBloomBox">Show the bubble</button>
  <div class="popover-bloom-box" id="popoverBloomBox" popover>The bubble blooms from its anchor</div>
</div>
CSS
.popover-bloom-stage,
.popover-bloom-stage *,
.popover-bloom-stage *::before,
.popover-bloom-stage *::after {
  box-sizing: border-box
}

.popover-bloom-stage {
  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;
}

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

.popover-bloom-btn:hover {
  background: #4338ca;
}

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

/* La bulle est un popover : elle vit dans la couche supérieure (jamais coupée par un overflow: hidden,
   jamais sous un z-index voisin) et se place au-dessus du bouton qui l'ouvre grâce à l'ancrage CSS
   (position-area). Sans ancrage CSS, le script la place lui-même. */
.popover-bloom-box {
  position: fixed;
  inset: auto;
  margin: 0 0 10px;
  position-area: top;
  border: 0;
  overflow: visible;
  background: linear-gradient(135deg, #4f46e5, #7c3aed);
  color: white;
  padding: 14px 22px;
  border-radius: 12px;
  font-family: sans-serif;
  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;
  pointer-events: none;
  transform-origin: bottom center;
  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),
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete;
}

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

/* Ouverte : état d'arrivée. @starting-style donne l'état de départ de l'entrée (le popover
   passe de display: none à affiché, il n'a pas d'état « avant » sans cette règle). */
.popover-bloom-box:popover-open {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}

/* Repli des navigateurs sans API Popover (règle séparée : un sélecteur :popover-open inconnu
   invaliderait toute la liste). */
.popover-bloom-box.popover-bloom-open {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}

@starting-style {
  .popover-bloom-box:popover-open {
    opacity: 0;
    scale: 0.6;
  }
}

@media (prefers-reduced-motion: reduce) {
  .popover-bloom-box {
    scale: 1;
    transition: opacity 0.15s ease, display 0.15s allow-discrete, overlay 0.15s allow-discrete;
  }

  @starting-style {
    .popover-bloom-box:popover-open {
      scale: 1;
    }
  }
}
JavaScript
(function() {
  /* Popover Bloom : l'attribut popovertarget ouvre et ferme la bulle sans script (Échap et clic
     à l'extérieur compris). Ce script ajoute aria-expanded, ancre chaque bulle à son bouton et
     prend le relais là où le navigateur ne connaît pas l'ancrage CSS ou l'API Popover. */
  var hasPopover = HTMLElement.prototype.hasOwnProperty('popover');
  var hasAnchor = !!(window.CSS && CSS.supports('position-area', 'top'));
  var count = 0;

  document.querySelectorAll('.popover-bloom-box').forEach(function(box) {
    var btn = box.id && document.querySelector('[popovertarget="' + box.id + '"]');
    if (!btn) return;
    btn.setAttribute('aria-expanded', 'false');

    function place() {
      var r = btn.getBoundingClientRect();
      box.style.left = (r.left + r.width / 2) + 'px';
      box.style.top = (r.top - 10) + 'px';
      box.style.translate = '-50% -100%';
      box.style.margin = '0';
    }

    if (!hasPopover) {
      /* Navigateur sans API Popover : bascule par classe, Échap ferme. */
      box.setAttribute('aria-hidden', 'true');
      var setOpen = function(open) {
        if (open) place();
        box.classList.toggle('popover-bloom-open', open);
        box.setAttribute('aria-hidden', String(!open));
        btn.setAttribute('aria-expanded', String(open));
      };
      btn.addEventListener('click', function() {
        setOpen(!box.classList.contains('popover-bloom-open'));
      });
      document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape') setOpen(false);
      });
      return;
    }

    if (hasAnchor) {
      /* Nom d'ancre propre à chaque bulle : plusieurs bulles cohabitent dans la page. */
      var name = '--popover-bloom-' + (++count);
      btn.style.setProperty('anchor-name', name);
      box.style.setProperty('position-anchor', name);
    }

    box.addEventListener('beforetoggle', function(e) {
      if (e.newState === 'open' && !hasAnchor) place();
    });
    box.addEventListener('toggle', function(e) {
      btn.setAttribute('aria-expanded', String(e.newState === 'open'));
    });
    if (!hasAnchor) {
      var follow = function() {
        if (box.matches(':popover-open')) place();
      };
      window.addEventListener('scroll', follow, true);
      window.addEventListener('resize', follow);
    }
  });
})();

Stage Curtain

The content appears once the curtains have closed.

Dialog Curtain

Dialog with stage curtains: the curtains meet in the center, then the content appears.

CSS Dialog Transition
Effect page →
View source code
HTML
<div class="dialog-curtain-stage">
  <div class="dialog-curtain-preview" aria-hidden="true">
    <div class="dialog-curtain-preview-left"></div>
    <div class="dialog-curtain-preview-right"></div>
    <span class="dialog-curtain-preview-label">Stage Curtain</span>
  </div>
  <button class="dialog-curtain-btn" type="button" aria-haspopup="dialog" aria-controls="dialogCurtainModal">Open the dialog</button>
  <dialog class="dialog-curtain-modal" id="dialogCurtainModal" aria-labelledby="dialogCurtainTitle">
    <div class="dialog-curtain-backdrop"></div>
    <div class="dialog-curtain-left"></div>
    <div class="dialog-curtain-right"></div>
    <div class="dialog-curtain-content">
      <h3 id="dialogCurtainTitle">Stage Curtain</h3>
      <p>The content appears once the curtains have closed.</p>
      <button class="dialog-curtain-close" type="button">Close</button>
    </div>
  </dialog>
</div>
CSS
.dialog-curtain-stage,
.dialog-curtain-stage *,
.dialog-curtain-stage *::before,
.dialog-curtain-stage *::after {
  box-sizing: border-box
}

.dialog-curtain-stage {
  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;
}

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

.dialog-curtain-btn:hover {
  background: #4338ca;
}

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

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

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

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

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

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

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

.dialog-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;
}

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

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

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

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

.dialog-curtain-modal {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  background: transparent;
  color: #fff;
  overflow: hidden;
  font-family: sans-serif;
}

/* Le fond assombri animé est .dialog-curtain-backdrop : celui du navigateur reste transparent. */
.dialog-curtain-modal::backdrop {
  background: transparent;
}

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

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

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

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

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

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

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

.dialog-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;
}

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

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

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

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

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

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

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

.dialog-curtain-close {
  padding: 8px 20px;
  background: #4f46e5;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  font-size: 0.85rem;
}

.dialog-curtain-close:focus-visible {
  outline: 2px solid #c7d2fe;
  outline-offset: 3px;
}

/* Mouvement réduit : pas de rideaux qui glissent, chaque élément prend directement son état. */
@media (prefers-reduced-motion: reduce) {
  .dialog-curtain-stage *,
  .dialog-curtain-modal *,
  .dialog-curtain-modal *::before,
  .dialog-curtain-modal *::after {
    transition: none !important;
  }
}
JavaScript
(function() {
  /* Dialog Curtain : un vrai <dialog> ouvert en modale (showModal) — le reste de la page devient
     inerte, le focus entre dans la fenêtre et revient au bouton à la fermeture. Les classes
     ne pilotent que l'animation en trois temps : fond, rideaux, puis contenu. */
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.dialog-curtain-btn[aria-controls]').forEach(function(btn) {
    var dialog = document.getElementById(btn.getAttribute('aria-controls'));
    if (!dialog || typeof dialog.showModal !== 'function') return;
    var stage = btn.closest('.dialog-curtain-stage');
    var preview = stage && stage.querySelector('.dialog-curtain-preview');
    var timer = null;

    function reset() {
      clearTimeout(timer);
      timer = null;
      dialog.classList.remove('dialog-curtain-open', 'dialog-curtain-closing');
    }

    function open() {
      if (dialog.open) return;
      reset();
      dialog.showModal();
      void dialog.offsetWidth; /* point de départ des transitions : rideaux ouverts, contenu caché */
      dialog.classList.add('dialog-curtain-open');
      if (preview) preview.classList.add('dialog-curtain-active');
    }

    function close() {
      if (!dialog.open || timer) return;
      dialog.classList.add('dialog-curtain-closing');
      if (preview) preview.classList.remove('dialog-curtain-active');
      timer = setTimeout(function() {
        dialog.close();
      }, reduce.matches ? 0 : 700);
    }

    btn.addEventListener('click', open);
    /* Une fois fermés, les rideaux recouvrent tout le fond : un clic dessus ferme aussi. */
    dialog.querySelectorAll('.dialog-curtain-close, .dialog-curtain-backdrop, .dialog-curtain-left, .dialog-curtain-right').forEach(function(el) {
      el.addEventListener('click', close);
    });
    /* Échap : la fermeture passe par l'animation inverse au lieu d'être immédiate. */
    dialog.addEventListener('cancel', function(e) {
      e.preventDefault();
      close();
    });
    /* Quelle que soit la façon dont la fenêtre se ferme, elle repart propre. */
    dialog.addEventListener('close', function() {
      reset();
      if (preview) preview.classList.remove('dialog-curtain-active');
    });
  });
})();

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 Keyframes

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

The card enters edge-on with a 3D rotation, then leaves by turning on in the same direction.

CSS 3D Rotation Backface

Fade Through Color

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

CSS JS Color Transition

Motion Design

Staggered Reveal

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

CSS JS Cascade
Effect page →
View source code
HTML
<div class="md-stagger-stage">
  <div class="md-stagger"></div>
</div>
CSS
.md-stagger-stage,
.md-stagger-stage *,
.md-stagger-stage *::before,
.md-stagger-stage *::after {
  box-sizing: border-box
}

.md-stagger-stage {
  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);
  }
}

/* Mouvement réduit : les éléments sont posés d'emblée, sans montée ni agrandissement */
@media (prefers-reduced-motion: reduce) {
  .md-stagger-item {
    animation: none;
    opacity: 1;
    transform: none;
  }
}
JavaScript
(function() {
  // Staggered Reveal : chaque .md-stagger de la page entre en cascade quand il
  // arrive à l'écran, puis reste affiché. Il se rejoue s'il sort de l'écran et y revient.
  var once = false; // true : la cascade ne se joue qu'à la première entrée

  document.querySelectorAll('.md-stagger').forEach(function(container) {
    // Vos éléments s'il y en a dans le conteneur, sinon 12 carrés de démonstration
    var own = container.children.length > 0;

    function play() {
      if (!own) {
        container.innerHTML = '';
        for (var i = 0; i < 12; i++) container.appendChild(document.createElement('div'));
      }
      Array.prototype.forEach.call(container.children, function(item, i) {
        item.classList.remove('md-stagger-item');
        void item.offsetWidth; // relance l'animation
        item.classList.add('md-stagger-item');
        item.style.animationDelay = (i * 0.1) + 's';
      });
    }

    if (!('IntersectionObserver' in window)) {
      play();
      return;
    }
    var obs = new IntersectionObserver(function(entries) {
      if (!entries[0].isIntersecting) return;
      play();
      if (once) obs.disconnect();
    }, {
      threshold: 0.3
    });
    obs.observe(container);
  });
})();

Elastic Spring

Spring animation with physical bounce and realistic elastic deformation.

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

.md-spring-stage {
  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);
  }
}

/* Mouvement réduit : ni saut ni déformation, une pulsation d'opacité garde le signal */
@media (prefers-reduced-motion: reduce) {
  .md-spring-ball {
    animation: mdSpringFade 2s ease-in-out infinite;
  }
}

@keyframes mdSpringFade {

  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.45;
  }
}
JavaScript
/* Pure CSS — no JavaScript required */

Orchestrated Motion

Eight bars that stretch in turn, each offset by 0.1 s, for a continuous wave in pure CSS.

CSS Keyframes 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
Effect page →
View source code
HTML
<div class="md-inertia-stage">
  <div class="md-inertia">
    <div class="md-inertia-box"></div>
  </div>
</div>
CSS
.md-inertia-stage,
.md-inertia-stage *,
.md-inertia-stage *::before,
.md-inertia-stage *::after {
  box-sizing: border-box
}

.md-inertia-stage {
  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;
}

/* Centré par le CSS (et non mesuré au chargement) : juste même si le bloc est masqué au départ.
   touch-action: none laisse le doigt saisir le cube au lieu de faire défiler la page. */
.md-inertia-box {
  width: 50px;
  height: 50px;
  border-radius: 10px;
  background: linear-gradient(135deg, #6366f1, #06b6d4);
  cursor: grab;
  position: absolute;
  left: calc(50% - 25px);
  top: calc(50% - 25px);
  touch-action: none;
  user-select: none;
  transition: none;
}
JavaScript
(function() {
  // Inertia Momentum : saisir, lancer, l'élément glisse puis ralentit (souris, doigt, stylet)
  var friction = 0.95;
  var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.md-inertia').forEach(function(container) {
    var box = container.querySelector('.md-inertia-box');
    if (!box) return;

    var x = 0,
      y = 0,
      vx = 0,
      vy = 0;
    var isDragging = false,
      lastX = 0,
      lastY = 0,
      raf = 0;

    function place() {
      box.style.left = x + 'px';
      box.style.top = y + 'px';
    }

    box.addEventListener('pointerdown', function(e) {
      isDragging = true;
      cancelAnimationFrame(raf);
      x = box.offsetLeft; // position réelle au moment de la saisie
      y = box.offsetTop;
      lastX = e.clientX;
      lastY = e.clientY;
      vx = 0;
      vy = 0;
      try { box.setPointerCapture(e.pointerId); } catch (err) {}
      box.style.cursor = 'grabbing';
      e.preventDefault();
    });

    box.addEventListener('pointermove', function(e) {
      if (!isDragging) return;
      vx = e.clientX - lastX;
      vy = e.clientY - lastY;
      x += vx;
      y += vy;
      place();
      lastX = e.clientX;
      lastY = e.clientY;
    });

    function release() {
      if (!isDragging) return;
      isDragging = false;
      box.style.cursor = 'grab';
      if (reduceMotion.matches) { // mouvement réduit : pas de glissade, l'élément est seulement ramené dans la piste
        x = Math.max(0, Math.min(container.clientWidth - box.offsetWidth, x));
        y = Math.max(0, Math.min(container.clientHeight - box.offsetHeight, y));
        place();
        return;
      }
      raf = requestAnimationFrame(animate);
    }
    box.addEventListener('pointerup', release);
    box.addEventListener('pointercancel', release);

    function animate() {
      if (isDragging) return;
      if (Math.abs(vx) < 0.1 && Math.abs(vy) < 0.1) return;
      vx *= friction;
      vy *= friction;
      x += vx;
      y += vy;
      var maxX = container.clientWidth - box.offsetWidth;
      var maxY = container.clientHeight - box.offsetHeight;
      if (x < 0 || x > maxX) vx *= -0.5;
      if (y < 0 || y > maxY) vy *= -0.5;
      x = Math.max(0, Math.min(maxX, x));
      y = Math.max(0, Math.min(maxY, y));
      place();
      raf = 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
Effect page →
View source code
HTML
<div class="md-char-stage">
  <div class="md-char-cascade"></div>
</div>
CSS
.md-char-stage,
.md-char-stage *,
.md-char-stage *::before,
.md-char-stage *::after {
  box-sizing: border-box
}

.md-char-stage {
  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);
  }
}

/* Le mot entier, lu par les lecteurs d'écran, invisible à l'œil */
.md-char-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Mouvement réduit : les lettres sont posées d'emblée, le mot change sans mouvement */
@media (prefers-reduced-motion: reduce) {
  .md-char {
    animation: none;
    opacity: 1;
    transform: none;
  }
}
JavaScript
(function() {
  // Character Cascade : le mot monte lettre par lettre, puis cède la place au suivant toutes les 2,5 s
  document.querySelectorAll('.md-char-cascade').forEach(function(container) {
    // Les mots : un élément enfant par mot (<span>VANNES</span><span>LORIENT</span>),
    // sinon la liste de démonstration
    var words = Array.prototype.map.call(container.children, function(el) {
      return el.textContent.trim();
    }).filter(Boolean);
    if (!words.length) words = ['MOTION', 'DESIGN', 'EFFECT', 'SMOOTH'];
    var currentWord = 0;
    var intervalId = null;

    function showWord() {
      // Animations mises en pause (bouton Pause de la page, outils d'accessibilité) : on garde le mot affiché
      var first = container.querySelector('.md-char');
      if (first && getComputedStyle(first).animationPlayState === 'paused') return;
      container.innerHTML = '';
      var word = words[currentWord];
      // Le mot entier pour les lecteurs d'écran ; les lettres, masquées pour eux, sont pour l'œil
      var label = document.createElement('span');
      label.className = 'md-char-sr';
      label.textContent = word;
      container.appendChild(label);
      word.split('').forEach(function(char, i) {
        var span = document.createElement('span');
        span.className = 'md-char';
        span.setAttribute('aria-hidden', 'true');
        span.textContent = char === ' ' ? '\u00a0' : char;
        span.style.animationDelay = (i * 0.08) + 's';
        container.appendChild(span);
      });
      currentWord = (currentWord + 1) % words.length;
    }
    showWord();
    if (words.length < 2) return;

    // Le cycle ne tourne que lorsque le bloc est visible
    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 {
      // Repli : cycle permanent
      intervalId = setInterval(showWord, 2500);
    }
  });
})();

3D Flip Sequence

On click, the cards flip in 3D one after another, with a delay between each card for a domino effect.

CSS 3D JS 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