Skip to content

Last updated: First published:

The Element Crossing

Transfer selected element states across cross-document view transitions.

The Element Crossing allows you to preserve state of the elements of your current page and carry it over to the next page when using cross-document view transitions. This also works independent of view transitions as long as all pages have the same origin. Tag your DOM elements for crossing and find their current dynamic state on the next page.

PREREQUISITE

State Loss on Cross-Document Transitions

You can easily activate and customize cross-document view transitions using just CSS! But keep in mind that swapping in a new page will reset all your current dynamic DOM state.

Check out this example to see which parts of the DOM are impacted:

  • The state of collapsed link groups
  • Changes to the color scheme
  • The position of the video and animation in the footer
  • The content of the form at the bottom of the page

In those cases, navigation not only results in a regrettable loss of information but can also be visually disrupt the seamless and smooth effect of view transitions.

Using the Element Crossing

The Element Crossing script helps you preserve state that matters and minimize visual disruptions. It comes in two flavors: Vanilla and Over-the-Top.

Here, Vanilla stands for reliable, well-understood state of the art.
And Over-the-Top is … well, a bit over the top: experimental, fancy and might not work at all in some settings.

From the feature list, there are only few difference:

  • Vanilla stores the state of DOM and CSS properties in the sessionStorage to move them to the new page across a full page load.
  • Over-the-Top can also preserve whole DOM trees across cross-document view transitions and offers a crossingStorage to preserve arbitrary JavaScript objects across navigation.

Coincidentally, the implementation of the crossingStorage literally works over the top (window).

Element Crossing Demo: Vanilla

The Vanilla demo showcases how this version of the Element Crossing can maintain state across cross-document view transitions. You can directly incorporate it into your own projects.

Element Crossing Demo: Experimental Over-the-Top

Experimental and maybe never mature for production: The Over-the-top demo demonstrates the additional features of the over-the-top version.

Installation

To use the Element Crossing, you can install the npm package in your project. Alternatively you can load the script from one of the global content delivery networks that provide npm packages.

  1. Install @vtbag/element-crossing from npm.
  2. In your project, add @vtbag/element-crossing/lib/vanilla.js as an inline script at the beginning of the <head> element on all pages where you want to retrieve or restore state during navigation.

Details depend on your project setup and the frameworks used, but with a bundler like vite it can be as simple as:

import crossing from "@vtbag/element-crossing?url";
<html>
<head>
<script src={crossing} />
...
</head>
...
</html>;

To experiment with the over-the-top flavor include the @vtbag/element-crossing/experimental and the @vtbag/element-crossing script in that order.

Vanilla Configuration

To move state to the next page, you need to tell the Element Crossing for which HTML elements to move state and what state to move. Assigning the data-vtbag-x attribute to identify the elements that should have some state carried over.

Expressions

The data-vtbag-x attribute accepts a string that has to fullfil the following grammar:

id:identifierspaceprop:class:style:anim:name

id

The id expression is used to link elements between the old and new pages, allowing state to be transferred from the current HTML element to the target element with the same id on the subsequent page. If an id value appears exclusively on either the old or the new page, no state transfer occurs. It’s important that id values remain unique on their respective pages. If the current HTML element already has an id attribute, the id expression within the data-vtbag-x string can be omitted.

prop

The prop expression identifies a property of the current HTML element. The value of this property is then transferred to the corresponding property of the target element on the next page.

class

The class expression specifies an entry in the current HTML element’s classList property. If the class is present on the current element, it will be added to the target element’s class list. Conversely, if the class is absent on the current element, it will be removed from the target element’s class list.

style

The style expression identifies an entry of the current HTML element’s style property. The value of this entry is transferred to the style property off the target element on the next page. This does not give access to CSS properties only available in computed styles.

anim

The anim expression references a CSSAnimation of the current HTML element. If the target element on the next page has an animation with the same animationName, the current playback time is transferred. Note these current restrictions of the Element Crossing: The animationName values must be unique per HTML element, and only CSSAnimations are supported, not CSSTransitions.

The anim expression can also be used to persist SVG animation states. Using the special anim:/svg expression, it transfers the current animation playback time to the target element. The expression needs to be declared on the <svg> element, not on the <animate> element.

Abbreviated Forms

The long forms id:, prop:, class:, style:, and anim: can be abbreviated as #, @, ., -, and ~, respectively. For example, instead of writing data-vtbag-x="id:group class:active prop:value", you could write data-vtbag-x="#group .active @value", or use a combination of both forms.

Applications with Real-World Examples

Although the expressions may seem basic, it’s amazing how easily you can create the desired effects. Address any CSS property or DOM element property, any CSS class, or CSS animation. Below is a table that outlines the expressions used in the example above:

EffectHTML (on the old and new page)
Do not reset the scroll position of the sidebar on navigation<nav data-vtbag-x="id:nav prop:scrollTop">
Keep the collapsed state of the navigation group<details data-vtbag-x="id:ng1 prop:open">
Carry over the current playback time of the video<video data-vtbag-x="id:video prop:currentTime">
Carry over the state of the footer animation, which is defined as animation: shift ...<div data-vtbag-x="id:footer anim:shift">
Keep the current dark CSS class, the data-theme property and the color-scheme CSS property<html data-vtbag-x="id:root class:dark prop:data-theme style:color-scheme">
Do not lose the input of the form elements or the textarea dimensions<input id="name" type="text" data-vtbag-x="prop:value">, <input id="interest1" type="checkbox" data-vtbag-x="prop:checked">, <textarea id="comments" data-vtbag-x="prop:value style:width style:height">

The values of the id expressions used here are arbitrary as long as they are unique per page and appear on both sides of the crossing.

Over-the-Top Features

The Over-the-Top flavor provides one additional expression for the data-vtbag-x attribute and an additional API similar to window.sessionStorage.

Additional elem Expression

The additional expression supported by the over-the-top mode is elem:, abbreviated as &.

elem

The elem expression specifies a name that is used as a key to store the current HTML element in window.crossingStorage and to replace the target element on the next page with that value.

The crossingStorage API

The API is made available as window.crossingStorage. It provides the following functions:

SignatureDescription
setItem(key: string, data: any): voidStore the data object under the given key.
getItem(key: string): anyRetrieve the data object stored under the given key.
removeItem(key: string): voidRemove the data object stored under the given key.
clear(): voidRemove all the data objects stored under any key.
pseudoAddress(object:object):stringReturns a random but fixed value that consistently identifies the object like 0xAB1234 (or 0x000000 if the object is null or undefined).

Stored values will survive a cross-document navigation and can be retrieved on the other side.

Applications with Real-World Examples

With the help of the elem: expression we can simplify the examples above in many cases.

Here are the earlier examples revisited with the new capabilities:

EffectHTML (on the old and new page)Explanation
Keep the collapsed state of all navigation groups / Do not reset the scroll position of the sidebar on navigation<nav data-vtbag-x="id:nav elem:nav prop:scrollTop">Copying the whole <nav> frees us from annotating every <details> as all the collapse states are retained with the <nav> element. But the scroll position gets lost when we move the <nav> from the old to the new page. Thus we still need to explicitly copy the scrollTop property
Carry over the current playback time of the video<video data-vtbag-x="id:video prop:currentTime">Same as before, no improvement here.
Carry over the state of the footer animation, which is defined as animation: shift ...<div data-vtbag-x="id:footer anim:shift">Animation state gets reset on re-insertion of the element. No advantage of using elem: here.
Keep the current dark CSS class and the color-scheme CSS property<html data-vtbag-x="id:root class:dark style:color-scheme">You don’t want to copy the whole page. What would be the point of navigation then?
Do not lose the input of the form elements or the textarea dimensions<form id="exampleForm" data-vtbag-x="&form">Now this is really an improvement! Instead of annotating all input fields, just preserve the whole form and re-insert it on the new page with all its state and content.

Know limitations

This first version has some known limitations.

  • Using the Over-the-Top flavored Element Crossing can corrupt the remaining browser history for pages within its coverage area if you leave and then re-enter that area using the browser’s back/forward navigation.

  • Initially, entries in sessionStorage were not cleared after use. This worked well for view transitions. However, when performing a full page reload, it became confusing to see the DOM differ from what was originally in the HTML file. The current solution is to clear the sessionStorage entry after each page load and skipping the value restoration for anything other than a cross-document view transition. Stay tuned for further updates.