Portals

A visual window for seamless cross-site experience

`whoami`

M. Ahsan Ayaz

GDE in Angular & Web Technologies

Senior Software Engineer @ Klarna

Let's see how we navigate between sites

What about an MPA (Multi-page-site) ?

Problem?

With classic navigations, users have to see a blank screen until the browser finishes rendering the destination.

It just feels disconnected

Solution?

Portals

Portals enable...

All that an <iframe> does but with extra sugar.

They allow you to navigate their contents

Preload the contents to provide seamless transitions.

How to get started with Portals?

You can try out Portals in Chrome Canary

Also found working on Chrome Version 77.0.3865.90

Test it out

Basic usage

// or using plain javascript
portal = document.createElement('portal');
portal.src = 'https://ahsanayaz.com';
// portal.style = '...your styles here...';
document.body.appendChild(portal);

// When the user touches the preview
// (embedded portal):
// do fancy animation, e.g. expand …
// and finish by doing the actual transition
portal.activate();
<!-- use in HTML like an <iframe> -->
<portal src="https://ahsanayaz.com"></portal>

A bit more fun

	// Adding some styles with transitions
    const style = document.createElement('style');
    const initialScale = 0.4;
    style.innerHTML = `
      portal {
        cursor: pointer;
        position:fixed;
        height: 500px;
        width: 500px;
        opacity: 0;
        box-shadow: 0 0 20px 10px #999;
        border-radius: 10%;
        transform: scale(${initialScale});
        bottom: calc(20px + 30% * 0.4 - 50%);
        left: calc(100% * 0.4 - 50%);
        top: 0;
        right: 0;
        margin: auto;
        z-index: 10000;
      }
      .portal-transition {
        transition:
          transform 0.7s,
          height 0.3s,
          width 0.3s,
          border-radius 0.1s,
          opacity 1.0s;
      }
      @media (prefers-reduced-motion: reduce) {
        .portal-transition {
          transition: all 0.001s;
        }
      }
      .portal-reveal {
        transform: scale(1.0);
        bottom: 0px;
        left: 0px;
        border-radius: 0;
        width: 100%;
        height: 100%;
      }
      .fade-in {
        opacity: 1.0;
      }
    `;
    const portal = document.createElement('portal');
    // Let's navigate into the WICG Portals spec page
    portal.src = 'https://ahsanayaz.com';
    // Add a class that defines the transition. Consider using 
    // `prefers-reduced-motion` media query to control the animation. 
    // https://developers.google.com/web/updates/2019/03/prefers-reduced-motion
    portal.classList.add('portal-transition');
    portal.addEventListener('click', evt => {
      // Animate the portal once user interacts
      portal.classList.add('portal-reveal');
    });
    portal.addEventListener('transitionend', evt => {
      if (evt.propertyName == 'transform') {
        // Activate the portal once the transition has completed
        portal.activate();
      }
    });
    document.body.append(style, portal);

    // Waiting for the page to load.
    // using setTimeout is a suboptimal way and it's best to fade-in
    // when receiving a load complete message from the portal via postMessage
    setTimeout(_ => portal.classList.add('fade-in'), 800);

The <portal> element

 
  • The HTML element itself
  • contains src attribute for target URL
  • Has activate method for activating the portal and navigating to it.
    It takes an optional argument as data to pass to the portal upon activation.
  • An interface (postMessage) for sending messages to the portal.

The portalHost interface

 
  • Adds a portalHost object to the window object.
  • Also provides the interface (postMessage) to send messages back to the portal host.

The portalActivateEvent interface

 
  • The event that fires when the portal is activated.
  • Has a function called adoptPredecessor which can be called to get the previous page as a <portal> element.

Detecting if the page is an embedded portal


if (window.portalHost) {
  /* Customize the UI when being
   * embedded as a portal
   */
}

Messaging between <portal> and portalHost

// Send message to the portal element
const portal = document.querySelector('portal');
portal.postMessage({someKey: someValue}, ORIGIN);

// Receive message via window.portalHost
window.portalHost.addEventListener('message', evt => {
  const data = evt.data.someKey;
  // handle the event
});

Activating the <portal> element and receiving the portalactivate event

/* You can optionally add data to the
 * argument of the activate function
 */
portal.activate({data: {'somekey': 'somevalue'}});

// when the activate happens
window.addEventListener('portalactivate', evt => {
  // Data available as evt.data
  const data = evt.data;   
  if (data.somekey) {...}
});

Retrieving the predecessor 

// Listen to the portalactivate event
window.addEventListener('portalactivate', evt => {
  // ... and creatively use the predecessor
  const portal = evt.adoptPredecessor();
  const element = document.querySelector('someElm');
  element.appendChild(portal);
});

Knowing your page was adopted as a predecessor


portal.activate().then(_ => {
  /* Check if this document was adopted
   * into a portal element.
   */
  if (window.portalHost) {
    /* You can start communicating with
     * the portal element
     * i.e. listen to messages
     */
    window.portalHost.addEventListener('message', evt => {
      // handle the event
    });
  }
});

How does it work?

Site 1

Portal

Portal

Portal animated

// Event fires after the portal on-click animation finishes
embedContainer.addEventListener('transitionend', (evt) => {
  // We wait until the top transition finishes
  if (evt.propertyName !== 'bottom') {
    return;
  }
  activateAfterAnimation();
});

Activate after animate

Portal activated

window.addEventListener('portalactivate', (evt: any) => {
  document.body.classList.add('portal-activated');
  initialY = evt.data.initialY;
  initialWidth = evt.data.initialWidth;
  // animate the audio controller
  this.setPortalActivatedUI(
    evt.adoptPredecessor()
  );
});

adoptPredecessor after portal activated to show in the background

Portal activated

Predecessor

Tap tap!

Predecessor activated

Navigating to predecessor

this.lightbox.addEventListener('click', evt => {
  this.setPredecessorActivateUI(initialY, initialWidth);
  const predecessor: any = document.querySelector('portal');
  predecessor.activate().then(_ => {
    this.setDefaultUI();
    this.setEmbedUI();
    document.body.classList.remove('portal-activated');
  });
});

Binding click handler to lightbox for activating predecessor

.activate()

.postMessage()

adoptPredecessor

predecessor.activate()

.postMessage()

Further study / resources

Thank You !

Senior Software Engineer

Klarna

Portals

By Ahsan Ayaz

Portals

  • 42
Loading comments...

More from Ahsan Ayaz