Experiment: Animating CSS position-area with View Transitions

Recording of the demo.

~

CSS Anchor Positioning is a powerful tool, but one of the things that you cannot do natively (yet) is animating the position-area property. This blog post introduces a technique to animate position-area changes using View Transitions.

~

🌟 This post is about CSS Anchor Positioning. If you are not familiar with the basics of it, check out this 30-min talk of mine to get up to speed.

~

The problem

When the browser chooses one of the position-try-fallbacks to apply to an anchored element, the position-area’s Computed Value changes to that chosen fallback. This change is abrupt and in response the anchored element simply jumps from the old position-area to the new position-area. This change can’t be animated using CSS because position-area is discretely animatable (or rather: it’s currently defined as “TBD”, see w3c/csswg-drafts#13577), so the value just flips midway the transition.

.tooltip {
	position: fixed;
	position-area: block-start;
	position-try-fallbacks: flip-block;
	transition: position-area 0.2s ease; /* This doesn’t do anything visually … */
}

~

The technique

Back in 2024 I explored a technique to automatically trigger a View Transition whenever a CSS property changes. For this I relied on @bramus/style-observer which is a StyleObserver that allows you to respond to Computed Value changes in JavaScript. In the StyleObserver’s callback, I reset the targeted element to the previously recorded value, and then start a View Transition to the new value of that property.

For Anchor Positioning specifically, it’s sufficient to monitor the position-area property, as its Computed Value changes whenever a new position-try-fallback gets applied. To make sure that the View Transition is started from the previously recorded position-area, the position-try must be unset temporarily at the start of the View Transition. Therefore, that property also needs to be monitored.

import StyleObserver, { ReturnFormat, NotificationMode } from "@bramus/style-observer";

const $element = document.querySelector('.tooltip');

let isBusy = false;
const styleObserver = new StyleObserver((mutations) => {
	// Prevent double runs
	if (isBusy) return;
	isBusy = true;

	const positionArea = mutations['position-area'];
		
	// No change, do nothing
	if (!positionArea.previousValue || !positionArea.changed) {
		isBusy = false;
		return;
	}
		
	// Move the element back its old location
	// This is done by forcing the old recorded positionArea
	// but most importantly by also unsetting the `position-try`
	$element.style.positionTry = 'none';
	$element.style.positionArea = positionArea.previousValue;

	// Restore the new positions
	const t = document.startViewTransition(() => {
		$element.style.positionTry = '';
		$element.style.positionArea = '';
	});

	isBusy = false;
},
{
	properties: ['position-area', 'position-anchor', 'position-try'],
	returnFormat: ReturnFormat.OBJECT,
	notificationMode: NotificationMode.ALL,
});

styleObserver.observe($element);

Here is a live demo that is using this technique:

See the Pen CSS Anchor Positioning: Animating `position-area` with View Transitions, powered by @bramus/style-observer by Bramus (@bramus) on CodePen.

Scroll the page up and down to trigger a different position-area on the positioned element.

~

Known Issues

Unfortunately there are some issues with the technique:

  1. The code does not work in Firefox.

    I initially thought this was because of position-area being underspecified in the spec – it’s animation type is currently specced as “TBD” – and that Firefox therefore did not mark the property as being Discretely Animatable. A look at Stylo’s source code tells me it is defined as a discretely animatable property so so that’s not the problem.

    Digging into the code of my StyleObserver, I see it only picks up the initially applied value of position-area but no subsequent changes, even though a getComputedStyle() indicates that the value did change. I think there is a bug on Firefox’s end in which it does not trigger a transition when the value changes as the result of a position-try-fallback being chosen.

    I have filed w3c/csswg-drafts#13577 at the CSSWG to fix the spec, and https://bugzilla.mozilla.org/show_bug.cgi?id=2020592 with Firefox to get the bug sorted on their end.

  2. Chrome is affected by a 1-frame glitch due to the ambiguously defined timing of transitionrun. w3c/csswg-drafts#11665 is concerned with this.

  3. In Safari, the transitionrun that tracks position-area keeps firing over and over once it has detected a change. This is fixed in Safari Technology Preview.

  4. While the View Transition is running, the positioned and anchored elements can feel a bit out-of-sync. This is because of how View Transitions deal with scroll: during a scroll, VTs retarget the end transform of the ::view-transition-group() pseudos, which makes them be subjected to the animation-duration instead of changing instantly. In w3c/csswg-drafts#10197 I am throwing around ideas to get this fixed.

All these issues are fixable over time, but I would say that the 1-frame glitch in Chrome is preventing this from being something that is really usable right now.

Also note that the anchor in the demo does not change aspect ratio when a new position-area gets set. If yours does, you’ll need this code by Jake.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

I can also be found on 𝕏 Twitter and 🐘 Mastodon but only post there sporadically.

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.