Go to homepage

Projects /

Immersive Transition Effect

In this tutorial, we will show you how to create an immersive transition effect between sections.

Check our new component library →



We can start creating two block elements, one used for a media item (e.g., video/image) and the other for generic content.

During the scrolling, the media element should get fixed once it reaches the center of the viewport. From that point on, it should scale up to animate in a full-screen element. Once the animation is complete, the content block should start scrolling over it.

<div class="immerse-section-tr js-immerse-section-tr">
  <div class="immerse-section-tr__media js-immerse-section-tr__media">
    <div class="container max-width-sm">
      <figure class="immerse-section-tr__figure js-immerse-section-tr__figure">
        <!-- video/img element here -->

  <section class="immerse-section-tr__content bg-transparent js-immerse-section-tr__content">
    <!-- some generic content here -->

To make the media element fixed during scrolling, we can use the sticky value of the CSS position property.

.immerse-section-tr__media {
  position: sticky;

Once the element gets fixed, we need to update its scale value based on the window scroll position. While creating the scale-up animation, we faced two main issues.

First: the scale animation happens while the user is scrolling; that means, during the animation, part of the .immerse-section-tr__content element (generic content block) was already visible.
To fix this issue, we can add to this element, a margin-top equal to the scrolling amount required by the animation. This makes sure no new content is visible during the scale animation.

Second: since we are scaling up the asset, we need to apply an overflow hidden to make sure a horizontal scrollbar is not visible during the animation. Unfortunately, the sticky position does not work if one of the parents has an overflow hidden. For this reason, we have to apply the overflow property directly to the element with a position sticky (.immerse-section-tr__media element).

For the overflow to work correctly, though, this element needs to have a height equal to the viewport height. Using JavaScript, we set its height equal to the viewport height and add a padding-top equal to half the difference between the viewport and the asset height (to make sure the asset element still gets fixed when it is at the center of the viewport). To balance the additional space created by the padding, we also add a negative top margin equal to this top padding. Done!

⚠️ Because of the restrictions explained above, the effect is disabled if:

  • video higher than the viewport; the scale-up effect would leave part of the asset inaccessible to users;
  • (viewport height - video height) is bigger than 600px. This would require a huge amount of additional scrolling and empty space below the asset, which would affect usability.

To complete the animation, we still need to:

  • detect when the media element enters the viewport to trigger the scale animation, using the Intersection Observer API;
  • animate the scale based on the scrolling value, using the window scroll event;
  • remove the scroll event listener once the element leaves the viewport.

We can create an ImmerseSectionTr object to implement the immersive transition effect:

var ImmerseSectionTr = function(element) {
  this.element = element;
  this.media = this.element.getElementsByClassName('js-immerse-section-tr__media');
  this.scrollContent = this.element.getElementsByClassName('js-immerse-section-tr__content');
  // other properties here
  this.scrollingFn = false;
  this.scrolling = false;

function initImmerseSectionTr(element) {
  updateMediaHeight(element); // update height of media element + set padding/margin values

  // observe when the element is sticky - update scale value and opacity layer 
  var observer = new IntersectionObserver(immerseSectionTrCallback.bind(element));

function immerseSectionTrCallback(entries) {
  if(entries[0].isIntersecting) {
    if(this.scrollingFn) return; // listener for scroll event already added
  } else {
    if(!this.scrollingFn) return; // listener for scroll event already removed
    window.removeEventListener('scroll', this.scrollingFn);
    this.scrollingFn = false;

function immerseSectionTrScrollEvent(element) {
  // animate scale value here

By default, the .immerse-section-tr__content element has no background-color (we use the .bg-transparent class to set its bg color to transparent).

The overlay effect is created using its ::before/::after pseudo-elements. Their opacity is controlled using a CSS custom property that can be updated in JavaScript:

.immerse-section-tr__content {
  position: relative;

  &::after, // overlay layer
  &::before { // section background
    content: '';
    pointer-events: none;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    background: var(--color-bg);
    opacity: var(--immerse-section-tr-opacity, 0);
    pointer-events: none;

  &::before {
    height: 100%;
    z-index: -1;

  &::after {
    height: 100vh;
    transform: translateY(-100%);

When the .immerse-section-tr__content element enters the viewport, the opacity is updated (from zero to one) to create the fade-in background effect.

🌄 Video credits:

Project duplicated

Project created

Globals imported

There was an error while trying to export your project. Please try again or contact us.