Search code examples
javascriptgoogle-chromewebcss-animationscss-transitions

How to use the View Transition API (web) for transition elements that are not the same DOM element?


I have a thumbnail DOM element that needs to transition into the modal DOM element that opens when that thumbnail is clicked. The thumbnail and the modal are NOT the same DOM element. For this I want to use the View Transitions API.

thumbnail-to-modal transition

The topic of using transition elements that are not the same DOM element is covered in one of the chapters of the Chrome article "Smooth and simple transitions with the View Transitions API".

I was surprised to find the transition element behave differently from what I expected, when trying to build the thumbnail-to-modal transition. What am I doing wrong?

I have written a simple variant of the code I expected to work in this codepen.

<!-- HTML -->
<div id="thumbnail">Click me</div>

<div id="modal">You have opened the modal<div id="closer">X</div></div>

<div id="comment">
pink outline = ::view-transition-old<br> green outline = ::view-transition-new"
</div>

/* CSS */
#thumbnail {
  position: relative;
  border: 2px solid darkred;
  display: inline-block;
  padding: 40px;
  cursor: pointer;
}

#thumbnail:hover {
  background-color: #74747444;
}

#modal {
  display: none;
  position: absolute;
  translate: -50% -50%;
  top: 50%;
  left: 50%;
  padding: 80px;
  border: 2px solid blue;
}

#closer {
  position: absolute;
  top: 4px;
  right: 4px;
  cursor: pointer;
}

::view-transition-old(modal),
::view-transition-new(modal) {
  animation-duration: 5s;
}

::view-transition-old(modal) {
  outline: 4px solid pink;
}

::view-transition-new(modal) {
  outline: 4px solid green;
  outline-offset: 4px;
}

// JavaScript
const thumbnail = document.querySelector("#thumbnail")

const modal = document.querySelector("#modal")

const closer = document.querySelector("#closer")

thumbnail.addEventListener("click", () => openModal())

closer.addEventListener("click", () => {
  modal.style.display = "none"
})

function openModal() {
  // Fallback for non-supporting browsers
  if (!document.startViewTransition) {
    modal.style.display = "block"
    return
  }
  
  thumbnail.style.viewTransitionName = "modal"
  modal.style.viewTransitionName = ""

  const transition = document.startViewTransition(() => {
    modal.style.display = "block"
  })
  
  transition.updateCallbackDone.then(() => {
    thumbnail.style.viewTransitionName = ""
    modal.style.viewTransitionName = "modal"
  })
  
  transition.finished.finally(() => {
    modal.style.viewTransitionName = ""
  })
}

Solution

  • Moving the code thumbnail.style.viewTransitionName = null; modal.style.viewTransitionName = "modal" from the updateCallbackDone promise callback to the startViewTransition callback did the trick.

    So the new code is as follows (also see this codepen).

    <div id="thumbnail">Click me</div>
    
    <div id="modal">You have opened the modal<div id="closer">X</div></div>
    
    #thumbnail {
      position: relative;
      border: 2px solid darkred;
      display: inline-block;
      padding: 40px;
      cursor: pointer;
    }
    
    #thumbnail:hover {
      background-color: #74747444;
    }
    
    #modal {
      display: none;
      position: absolute;
      translate: -50% -50%;
      top: 50%;
      left: 50%;
      padding: 80px;
      border: 2px solid blue;
      background-color: white;
    }
    
    #closer {
      position: absolute;
      top: 4px;
      right: 4px;
      cursor: pointer;
    }
    
    const thumbnail = document.querySelector("#thumbnail")
    
    const modal = document.querySelector("#modal")
    
    const closer = document.querySelector("#closer")
    
    thumbnail.addEventListener("click", () => openModal())
    
    closer.addEventListener("click", () => {
      modal.style.display = "none"
    })
    
    function openModal() {
      // Fallback for non-supporting browsers
      if (!document.startViewTransition) {
        modal.style.display = "block"
        return
      }
      
      thumbnail.style.viewTransitionName = "modal"
    
      const transition = document.startViewTransition(() => {
        thumbnail.style.viewTransitionName = null
        modal.style.viewTransitionName = "modal"
        modal.style.display = "block"
      })
        
      transition.finished.finally(() => {
        modal.style.viewTransitionName = null
      })
    }