w3c / w3c/csswg-drafts

[scroll-animations-1] Allow `<length-percentage>` in `<keyframe-selector>` when combined with `<timeline-range-name>`

Open
#10,000 12 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Closed Accepted by CSSWG Resolution css-animations-2 Needs Edits scroll-animations-1
Dominant language
Bikeshed
Stars
4.9k
Forks
816
PR merge metrics
PR metrics pending

Description

The problem

With Scroll-Driven Animations, authors often want to run animations offsetted against one of the segment edges. For example, to run an animation up to "100px from the start of the range". For this, authors can do calculations in keyframe offsets, e.g. animation-range: entry 0% entry calc(0% + 100px);.

While this syntax works perfectly fine, it is not allowed everywhere. Depending on which keyframes format authors use, they can or can’t do calculations.

  • WAAPI with keyframes as an object with rangeStart + rangeEnd set in the options: ✅ Allowed
  • WAAPI with keyframes as an array of objects that include range information: ❌ Not allowed
  • CSS Animations with animation-range-start + animation-range-end: ✅ Allowed
  • CSS Animations with @keyframes that include range information: ❌ Not allowed

This leads to frustration, as recently expressed by Matthew Perry on Twitter:

It's stuff like this that leads people back to using animation libraries. And these will only be more bloated if they have to jump through hoops like converting one animation into multiple animations to support multistop

I think we should close this gap, and allow calculated keyframes across the board.

Performing calculations in the offsets

Web Animations API

If they want to – and they often do – authors can also do calculations in the offset value. When passing in keyframes as an object, they can adjust the values for rangeStart and/or rangeEnd:

document.querySelectorAll('#list-view li').forEach($li => {
	const timeline = new ViewTimeline({
		subject: $li,
		axis: 'block',
	});

	$li.animate({
		opacity: [ 0, 1 ],
		transform: [ 'translateY(100%)', 'translateY(0)'],
	}, {
		fill: 'forwards',
		timeline,
		rangeStart: 'entry 0%',
		rangeEnd: 'entry calc(0% + 100px)', // 👈 This line
	});

	$li.animate({
		opacity: [ 1, 0 ],
		transform: [ 'translateY(0)', 'translateY(-100%)'],
	}, {
		fill: 'forwards',
		timeline,
		rangeStart: 'exit calc(100% - 100px)', // 👈 This line
		rangeEnd: 'exit 100%',
	});
});

However, when passing in an array of objects as the keyframes, it is not accepted.

document.querySelectorAll('#list-view li').forEach($li => {
	const timeline = new ViewTimeline({
		subject: $li,
		axis: 'block',
	});

	$li.animate([
		{ opacity: 0, offset: 'entry 0%' },
		{ opacity: 1, offset: 'entry calc(0% + 100px)' }, // ❌ Does not work
		{ opacity: 1, offset: 'exit calc(100% - 100px)' }, // ❌ Does not work
		{ opacity: 0, offset: 'exit 100%' },
	], {
		fill: 'both',
		timeline,
	});
});

In both snippets above, authors get back this error:

Uncaught TypeError: Failed to execute 'animate' on 'Element':
timeline offset must be of the form [timeline-range-name]

CSS Animations

Looking at the CSS variant of SDA, it’s similar there: when using animation-range an author can do calculations, but when creating a set of keyframes with @keyframes they can not.

@keyframes f {
  from { opacity: 0; }
  to { opacity: 1; }
}

el {
  animation-name: f;
  animation-timeline: view();
  animation-range: entry 0% entry calc(0% + 100px); /* ✅ Allowed */
}
@keyframes f {
  entry 0% { opacity: 0; } /* ✅ Allowed */
  entry calc(0% + 100px) { opacity: 1; } /* ❌ Not allowed */
}

el {
  animation-name: f;
  animation-timeline: view();
}

The Cause

In both cases (CSS or WAAPI), this is because of the syntax of keyframe selectors. As per spec, the offsets are limited to <percentage>s when combined with a <timeline-range-name>:

<keyframe-selector> = from | to | <percentage [0,100]> | <timeline-range-name> <percentage>

This in contrast to animation-range-start and animation-range-end which do accept lengths (spec):

animation-range-start = [ normal | <length-percentage> | <timeline-range-name> <length-percentage>? ]#

Proposed Solution

Allow <length-percentage> – instead of only <percentage> – in <keyframe-selector> when combined with <timeline-range-name>.

By allowing <length-percentage>, authors can perform these calculations across the board, independent of which format they use to create their animation. The proposed new syntax is this:

<keyframe-selector> = from | to | <percentage [0,100]> | <timeline-range-name> <length-percentage>

As a result, the failing code snippets listed above will start to work fine.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Scroll-Driven Animations specification's named-range-keyframes section and compare its keyframe-selector grammar with the animation-range-start and animation-range-end grammar cited in the issue. Review the discussion and determine whether the proposed length-percentage syntax is accepted; done means the specification consistently reflects the resolved decision and the examples match it.

Written by the indexing model from the issue text.

Assessment

Tech stack
css
Domain
frontend, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.