Skip to content

[css-view-transitions] Behavior of calling document.startViewTransition synchronously more than once #11292

Closed

Description

@bramus

Demo: https://codepen.io/bramus/pen/gOVyzjL

This demo has two successive calls to document.startViewTransition:

document.querySelectorAll('.item label').forEach(($label) => {
	// @note: we listen on click because that happens *before* a change event
	$label.addEventListener('click', async (e) => {
		if (!document.startViewTransition) return;

		e.preventDefault();
		const $input = $label.querySelector('input');
		
		document.startViewTransition(() => {
			$input.checked = !$input.checked;
		});

		document.startViewTransition(() => {
			document.body.style.padding = `${Math.random() * 10}rem`;
		});
		
	});
});

When you check this in Chrome, the code from both callbacks get executed as part of the View Transition: the faux <details> opens/closes using an animation + it also moves around because the padding of the <body> was changed.

When you check this in Safari, only the code from the second callback gets executed as part of the View Transition: the faux <details> is immediately opened/closed and only moves around because the padding of the <body> was changed.

As per spec, the second call to document.startViewTransition should skip any active view transition, which led me to believe Safari is doing the right thing here.

Checking things with @noamr, he says it’s racy in the spec:

This is actually racy in the spec:

  1. const transition1 = document.startViewTransition(updateCallback1);
  2. document.startViewTransition(updateCallback2):
    2.1 calls transition1.skipTransition() internally
    2.1.1 queue a task to call updateCallback1
    2.2 wait till the next rendering steps
    2.2.1 capture the old state
    2.2.2 queue a task to call updateCallback2

There is no guarantee that 2.2.1 would be after/before 2.1.1. It's a race between the rendering steps and the queued update callback.

I’m not sure if an how we can solve, but I surely would love to see all implementations behave the same by having intstructions for what to do in this case baked into the spec.

Activity

  1. noamr commented on Nov 28, 2024

    @noamr
    Collaborator

    The best practice here would be to use updateCallbackDone:

    const $input = $label.querySelector('input');
    
    await document.startViewTransition(() => {
    	$input.checked = !$input.checked;
    }).updateCallbackDone;
    
    document.startViewTransition(() => {
    	document.body.style.padding = `${Math.random() * 10}rem`;
    });
  2. bramus commented on Nov 28, 2024

    @bramus
    ContributorAuthor

    The best practice here would be to use updateCallbackDone

    Sure, that would fix that demo so that it’s consistent across browsers. But I also would love to see it work interoperably without that fix.

    I’m fine with whatever the outcome is (altough I expected Safari’s behavior to occur; but also see that Chrome’s behavior seems interesting). My main concern is interop here.

  3. noamr commented on Nov 28, 2024

    @noamr
    Collaborator

    An important detail here is that update callbacks can also be asynchronous and have multiple steps; we can queue them in such a way that they would start in the order of their respective view transitions, but without awaiting it would be constrained to a subset of those callbacks.

  4. noamr commented on Nov 28, 2024

    @noamr
    Collaborator

    @emilio @jakearchibald @nt1m any takes on this?
    I think relying on updateCallbackDone is the right thing to do here, but I'm open to exploring queuing the update callbacks in order received rather than race between the successful one and failed ones

  5. jakearchibald commented on Dec 3, 2024

    @jakearchibald
    Contributor

    The Safari implementation is definitely wrong, as it violates the intent that transitions are an enhancement.

    That means a failure to create a visual transition, which can happen due to misconfiguration or device constraints, will not prevent the developer’s UpdateCallback being called, even if it’s known in advance that the transition animations cannot happen.

    For example, if the developer calls skipTransition() at the start of the view transition lifecycle, the steps relating to the animated transition, such as creating the view transition tree, will not happen. However, the UpdateCallback will still be called. It’s only the visual transition that’s skipped, not the underlying state change.

    It'd be nice to remove the race condition somehow though.

  6. noamr commented on Dec 3, 2024

    @noamr
    Collaborator

    The Safari implementation is definitely wrong, as it violates the intent that transitions are an enhancement.

    Ah right, I was reading it as Safari was executing them in order. Not calling the update callback at all is definitely wrong and a spec violation. Surprised that we don't have WPTs for this.

    It'd be nice to remove the race condition somehow though.

    Happy to resolve on something and spec it. It's not complex to fix.

  7. noamr commented on Dec 3, 2024

    @noamr
    Collaborator

    Proposing the following resolution: the update callbacks would be maintained in an internal queue rather than separately on the event loop, and when "calling the update callback" all of them would be invoked.

  8. bramus commented on Dec 3, 2024

    @bramus
    ContributorAuthor

    The Safari implementation is definitely wrong, as it violates the intent that transitions are an enhancement.

    That means a failure to create a visual transition, which can happen due to misconfiguration or device constraints, will not prevent the developer’s UpdateCallback being called, even if it’s known in advance that the transition animations cannot happen.
    For example, if the developer calls skipTransition() at the start of the view transition lifecycle, the steps relating to the animated transition, such as creating the view transition tree, will not happen. However, the UpdateCallback will still be called. It’s only the visual transition that’s skipped, not the underlying state change.

    But Safari is executing the first callback (the one that changes .checked) just fine. What is different between Chrome and Safari is that Safari seems to flush the queue of previously registered callbacks before taking the snapshots, whereas Chrome executes all previously registered callbacks as part of the resulting transition.

    One could say Chrome is in the wrong here, because it changes the .checked state as part of the View Transition, which goes against “It’s only the visual transition that’s skipped, not the underlying state change.” – Chrome does not skip the visual transition.

  9. jakearchibald commented on Dec 3, 2024

    @jakearchibald
    Contributor

    When you check this in Safari, only the code from the second callback gets executed as part of the View Transition

    Ah, my bad, I interpreted the above incorrectly.

    What is different between Chrome and Safari is that Safari seems to flush the queue of previously registered callbacks before taking the snapshots, whereas Chrome executes all previously registered callbacks as part of the resulting transition.

    Yeah, I agree that's nice.

  10. bramus commented on Dec 3, 2024

    @bramus
    ContributorAuthor

    What is different between Chrome and Safari is that Safari seems to flush the queue of previously registered callbacks before taking the snapshots, whereas Chrome executes all previously registered callbacks as part of the resulting transition.

    Yeah, I agree that's nice.

    The Safari behavior?

  11. jakearchibald commented on Dec 3, 2024

    @jakearchibald
    Contributor

    Yes. Sorry, that wasn't clear. The Safari behaviour where it makes the previous transition do all its cancelation behaviours before the new transition does any of its capturing sounds good.

  12. noamr commented on Jan 21, 2025

    @noamr
    Collaborator

    Let's tag this for async resolution? @astearns

  13. bramus commented on Jan 21, 2025

    @bramus
    ContributorAuthor

    PROPOSED RESOLUTION: When starting a new view transition, have the previous active transition do all its cancelation behaviours before the new transition does any of its capturing.

  14. 3 remaining items

  15. nt1m commented on Jan 21, 2025

    @nt1m
    Member

    Sounds good

  16. moved this to FTF agenda items in CSSWG January 2025 meetingon Jan 22, 2025
  17. moved this from FTF agenda items to Regular agenda items in CSSWG January 2025 meetingon Jan 22, 2025
  18. astearns commented on Feb 4, 2025

    @astearns
    Member

    The CSSWG will automatically accept this resolution one week from now if no objections are raised here. Anyone can add an emoji to this comment to express support. If you do not support this resolution, please add a new comment.

    Proposed Resolution: When starting a new view transition, have the previous active transition do all its cancelation behaviours before the new transition does any of its capturing.

  19. astearns commented on Feb 10, 2025

    @astearns
    Member

    RESOLVED: When starting a new view transition, have the previous active transition do all its cancelation behaviours before the new transition does any of its capturing.

  20. added a commit that references this issue on Feb 11, 2025
    18d50d7
  21. added a commit that references this issue on Feb 18, 2025
    cc56543
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions