A visual window for seamless cross-site experience


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) ?


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

It just feels disconnected



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...';

// When the user touches the preview
// (embedded portal):
// do fancy animation, e.g. expand …
// and finish by doing the actual transition
<!-- 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;
        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 {
          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.addEventListener('click', evt => {
      // Animate the portal once user interacts
    portal.addEventListener('transitionend', evt => {
      if (evt.propertyName == 'transform') {
        // Activate the portal once the transition has completed
    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');

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 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') {

Activate after animate

Portal activated

window.addEventListener('portalactivate', (evt: any) => {
  initialY = evt.data.initialY;
  initialWidth = evt.data.initialWidth;
  // animate the audio controller

adoptPredecessor after portal activated to show in the background

Portal activated


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(_ => {

Binding click handler to lightbox for activating predecessor






Further study / resources

Thank You !

Senior Software Engineer



By Muhammad Ahsan Ayaz


  • 341
Loading comments...

More from Muhammad Ahsan Ayaz