Closed fantasai closed 1 year ago
The CSS Working Group just discussed scope of named timelines
.
Can we use this proposal to simplify the lookup of animation-timeline even further? I.e. if we can declare names on an ancestor and bind them later then we don't need the previous sibling lookup right? Just to make sure I understand this proposal fully is this something like:
<style>
#parent {
scroll-timeline-name: scroller;
scroll-timeline-attachment: defer;
}
#scroller {
scroll-timeline-name: scroller;
scroll-timeline-attachment: ancestor;
}
#animated {
animation-timeline: scroller;
}
</style>
<div id="parent">
<div>
<div id="animated"></div>
</div>
<div id="scroller"></div>
</div>
Agenda+ to discuss @flackr's suggestion to narrow the default scope to ancestors.
@andruud any concerns with the proposal to instead allow pre-declaring a scroll/view timeline in an ancestor allowing the use of scrollers that have not been discovered yet? I imagine this may introduce some complexity to the implementation but it is certainly nice from an authoring POV, and would mean that we could eliminate the sibling scope visibility since authors could always move the declaration of the name to an ancestor element instead.
@flackr That seems possible only if the attachment (i.e. connection between scroller and timeline) is part of the timeline's snapshot (that's taken at the start of the frame). That means that any "dynamic reattachment" of scrollers would not take effect until the next frame. This seems aligned with how scroll timelines work already, so should be totally fine IMO.
If we can remove sibling scope visibility because of this, then it might be a net positive in terms of impl. complexity. :-)
The CSS Working Group just discussed [scroll-animations] Broader scope of scroll timelines
, and agreed to the following:
RESOLVED: Reduce default scoping to ancestors only, add scroll-timeline-attachment as proposed in the issue
In true staircase wit, thinking things over last night, I would like to reconsider – at least discuss things a bit more.
As it stands now, with scroll-timeline-attachment
, an author needs to do three adjustments to hook onto a scroller that’s not a parent:
scroll-timeline-name
therescroll-timeline-attachment
to defer
on that ancestorscroll-timeline-attachment
to ancestor
on the scrollerThis is pretty verbose imo, and I think this should be shorter. Only step 1 should be necessary.
Looking at CSS Toggles, it uses a toggle-root
property to establish a toggle. Maybe we could use something similar for Scroll Timelines, e.g. scroll-timeline-root
?
With it, only one adjustment would be needed by authors:
scroll-timeline-root
thereReworking flackr’s example, the resulting code would then look like this:
<style>
#parent {
scroll-timeline-root: scroller; /* Establish scope for `scroller` */
}
#scroller {
scroll-timeline-name: scroller;
}
#animated {
animation-timeline: scroller;
}
</style>
<div id="parent">
<div>
<div id="animated"></div>
</div>
<div id="scroller"></div>
</div>
Added benefit is that there’s less room for confusion: by using a separate property, you can easily see you’re only declaring the scroll-timeline.
For author convenience, we can maybe also reconsider for the lookup of timelines to remain “up and out” instead of ancestors only? That way no scroll-timeline-root
is needed when attaching to a preceding sibling.
@bramus I agree the original syntax is pretty verbose at the moment and I think the main thing we want is just to solve the use case. Your proposal looks like a nice improvement and seems workable to me. So defining scroll-timeline-name in the absence of a root is an implicit root right?
A couple questions came up from @andruud:
I think it's reasonable to consider this an error (i.e. results in no timeline). In general a developer should have a particular scroller they are intending to observe when they declare a deferred scroll timeline name.
I believe there are strong use cases for observing non-ancestor timelines for both scroll timelines and view timelines so we should have it for both.
So defining scroll-timeline-name in the absence of a root is an implicit root right?
Correct.
- Handling multiple timelines attempting to attach to the same deferred timeline is complicated. Can we consider this an error?
Could the cascade help here to determine the winner? Sure, it’s not the same element that’s being targeted here, but it’s the same scroll-timeline-root that’s being targeted. Totally fine with deferring this, as it would only complicate things for now.
- Is this intentionally only for scroll timelines? I believe there are strong use cases for observing non-ancestor timelines for both scroll timelines and view timelines so we should have it for both.
Not quite following here. How can you track an element’s position within a non-parent scroller? It’s view-timeline inside that non-parent scroller would always be inactive, no?
@fantasai Could you shine your light on this? We have video-documentation on this that is being in the first week of April, so a resolution is urgent.
Not quite following here. How can you track an element’s position within a non-parent scroller? It’s view-timeline inside that non-parent scroller would always be inactive, no?
No, the named view timeline would still track some element's progress within its nearest scroller, but that view timeline could drive an animation on another element.
Oh yeah, of course – I was too focused on animating the subject itself 🤦♂️. Makes sense to add it.
This is pretty verbose imo, and I think this should be shorter.
@bramus I don't think it's very verbose?
.root { scroll-timeline: carousel defer; }
.scroller { scroll-timeline: carousel ancestor; }
.animator { animation-timeline: carousel; }
I'm not super against using a separate property to declare a scope, but having a handshake like this reduces conflicts like some other element defining timeline with the same name and expecting it to bind locally. It also allows for recursion, which re-using the same property doesn't.
For author convenience, we can maybe also reconsider for the lookup of timelines to remain “up and out” instead of ancestors only? That way no scroll-timeline-root is needed when attaching to a preceding sibling.
There were two benefits to removing the sibling lookup:
This is pretty verbose imo, and I think this should be shorter.
@bramus I don't think it's very verbose?
.root { scroll-timeline: carousel defer; } .scroller { scroll-timeline: carousel ancestor; } .animator { animation-timeline: carousel; }
When put into the shorthand is looks pretty concise indeed. I’m OK with this.
There were two benefits to removing the sibling lookup:
- Improves performance and implementation simplicity.
- Removes asymmetry between previous and next siblings, which could cause the author to order things unnaturally in the source (which affects a11y and other operations on the source tree).
Got it. That first bullet sold it to me :)
Consider me convinced of what we resolved on, with the explicit mention that the scroll-timeline-attachment
can go in the shorthand.
Still a few minor details/questions:
scroll-timeline
shorthand would then become this?
[ <'scroll-timeline-name'> [<'scroll-timeline-axis'> || <'scroll-timeline-attachment'>]? ]#
view-timeline-attachment
property for View Progress Timelines?
[ <'view-timeline-name'> [<'view-timeline-axis'> || <'view-timeline-attachment'>]? ]#
closest
might need a better name as that might imply sibling at position x+1 (i.e. the next sibling) would be matched instead preceding sibling at position x-2.@bramus so I think we're good to go on this, Re (1) the spec text should include it in the timeline shorthand, and (2) we should support this for view timelines as well. Re (3), we can get rid of closest if we no longer need previous sibling scope, right?
I'd also like to confirm that we can treat multiple timelines binding to the same deferred name as an error resulting in no timeline.
Sounds perfect!
@fantasai What happens to scroll-timeline-axis
on the deferred timeline? Is it just ignored?
.root {
scroll-timeline-name: timeline;
scroll-timeline-axis: block;
scroll-timeline-attachment: defer;
}
.root .inner {
scroll-timeline-name: timeline;
scroll-timeline-axis: inline;
scroll-timeline-attachment: ancestor;
}
The same question goes for view-timeline-inset
.
@andruud That's a good question. I have a few thoughts:
scroll-timeline-source: <name>
and view-timeline-subject: <name>
which set the current element as the source/subject for the named scroll/view timeline.scroll-timeline
and view-timeline
shorthands such that you can't specify both defer and other axis/inset properties. This makes it less likely that the authors think they're applying them.Let's try to deviate as little as possible from the resolution. :P
It's probably non-catastrophic to stick with the API as resolved currently, but specify that it's the axis/inset from the non-deferred timeline that's used when calculating the (deferred) timeline state.
Or, specify that it's the deferred timeline that decides which axis (etc) to use, but add something like scroll-timeline-axis:auto
(initial value), which means "take the value from the non-deferred timeline" if -attachment
is defer
(otherwise block
). Doing flackr@'s suggestion (2) above may then make (even more) sense, to avoid accidental axis "overrides" via the shorthand.
@fantasai which way did you think that it should work? Taking the extra property values (axis, inset) from the ancestor whose timeline-attachment is defer
or from the child whose attachment is ancestor
? Naively I assumed the ancestor is only reserving the name and so we'd use the inset / axis values from the child element declaring the timeline.
@flackr I hadn't thought about it, actually. I'm inclined to prioritize values from the child, since it's local and it knows what direction it's primarily scrolling in. :)
I'm also OK with adding a value that looks up to the declaring element. If we want to make that the default behavior, though, we need to decide that now... I'm not really sure if it's a good idea or not, though; defaulting that way let's the author choose whether they want to control the axis locally or from the top, but as @flackr notes, the action-at-a-distance could be a source of errors. @bramus, any thoughts?
Because scroll-timeline-axis
doesn’t inherit, I’d say ignore it on the parent.
Consider this snippet:
.parent {
scroll-timeline-name: timeline;
scroll-timeline-axis: inline;
scroll-timeline-attachment: defer;
}
.parent .child {
scroll-timeline-name: timeline;
scroll-timeline-attachment: ancestor;
}
It’d be weird to see the computed value for scroll-timeline-axis
on the child be inline
, when said property doesn’t inherit and has a default value of block
. Magically taking over the value from the parent looks like something that doesn’t rhyme with the rest of how CSS works.
@fantasai @flackr @bramus I attempted to spec this here: https://github.com/w3c/csswg-drafts/pull/8680
@andruud Left some review comments!
@flackr @bramus Wrt multiple scroll containers attempting to attach to the same name: should we be erroring this case to nothing, or taking the last one in tree order? CSS tends to use the "take the last one" principle to resolve conflicts in most places...
@flackr @bramus Wrt multiple scroll containers attempting to attach to the same name: should we be erroring this case to nothing, or taking the last one in tree order? CSS tends to use the "take the last one" principle to resolve conflicts in most places...
Given the expectation that the developer has chosen a particular timeline to be elevated / visible at the ancestor we expect that they have a 1:1 pairing of ancestor/descendant. I think treating multiple descendants trying to slot into the same name as an error would help bring the issue to the developer's attention when this happens accidentally. I feel like it would be unlikely for developers to identify multiple scroll timelines intentionally. That said I do agree that it goes against the usual convention - but the usual convention with properties is also the one from the selector with most specificity which this does not do.
@flackr @bramus Wrt multiple scroll containers attempting to attach to the same name: should we be erroring this case to nothing, or taking the last one in tree order? CSS tends to use the "take the last one" principle to resolve conflicts in most places...
I can imagine situations where authors would want the last inserted element to take over. For example, take a scroller where new elements are added are time (e.g. a chat interface), which all want to set the view-timeline. Here, I would expect the last inserted element to take over – irrelevant of its visual position within the scroller or its sequence in the tree.
Demo: https://codepen.io/bramus/pen/wvYvqQm/a56191f447a889bd2640d0b81d7f9a70 – Click to add more boxes. The last added box (the one with the golden border) is the one that determines the view-timeline
for all other .subject
elements.
The demo fakes scroll-timeline-attachment
by a) inserting the new node before all the other nodes and b) leveraging the fact that Chrome can (for the time being) still look up preceding siblings their timelines. With the suggested changes from this issue – and preceding sibling lookup on its way out anyway – it should not matter where in the tree the new .subject
is added, as the whole attachment thing should take care of it.
You could of course say the author needs to more narrowly scope the element they want to target by leveraging :nth-last-child(1)
or the like, so that only 1 element is matched in the first place. However, could also be that 2 separate selectors target 2 different elements, by which you’d end up in a similar situation and the cascade can offer no further help.
So I guess I’m suggesting: “last added element wins, no questions asked.” On load that would be the tree order (I suppose?), after that it’s dynamically. I think this would match with the expectations authors have.
Would that be possible implementation-wise, @andruud? If I’m doing a dumb suggestion here, then I’m definitely OK with erroring out in this particular case.
Last added would likely be error prone / a compat risk as it depends on when style updates happen. E.g. if you add a node before the existing nodes have been styled it doesn't keep track of the order the nodes were added in at style time.
Last in defined DOM order is possible as it has a defined answer, but adds non-trivial implementation complexity. When we detect a new timeline we have to determine whether it is earlier or later than the previously defined ones. Similarly when the currently attached timeline is removed we'd have to find the latest previous one to attach to.
Allrighty, noted. In that case I’m definitely fine with erroring out. In such cases, authors should try to use more specific selectors and/or media/container queries, so that only one node at a time wants to set the timeline.
Sorry, I've been discussing this issue in private channels and haven't taken the time to raise my concerns publicly. Let's get that fixed. ^_^
I find the current proposed API shapes (-name, -axis, and -attachment) to be intensely confusing. The -attachment values are not modifying some properties of the "animation", but rather setting a mode on the element that changes how they use the -name and -axis properties. I believe this is caused by an accumulative design; we'd already designed a "create animation, attach scroller to it" on a single element, and tried to do a minimum delta into the new model of those two things being separate.
Since we're not stuck with the prior properties yet, I think we should redesign slightly to better accommodate the model as we now understand it.
There are two separate things we want to do:
My syntax proposal borrows from Bramus's:
scroll-timeline-root: [ <timeline-name> && ancestor? ]#
scroll-timeline-axis: [ none | <axis-keywords> ]#
Each property has two possibilities: scroll-timeline-root
can either create a new timeline name, or refer to an ancestor's timeline name; scroll-timeline-axis
can either attach an axis to the timeline specified by -root
, or not.
The shorthand possibilities, then, are:
scroll-timeline: foo;
creates a foo
timeline, but doesn't attach a scroll axis to it; it's assumed a descendant will do so.scroll-timeline: foo block
; creates a foo
timeline and attaches the element's scrolling block axis to itscroll-timeline: foo ancestor block;
attaches the element's scrolling block axis to a foo
timeline established by an ancestorscroll-timeline: foo ancestor none;
does nothing. We might want to write out the shorthand grammar explicitly to disallow this (rather than just using <'scroll-timeline-root'> || <'scroll-timeline-axis'>
We could eliminate the dead possibility by instead specifying ancestor
in scroll-timeline-axis
instead. That is:
scroll-timeline-name: <timeline-name>#
scroll-timeline-axis: [ <axis-keywords> && ancestor? ]#
But this means the -axis
value is changing the interpretation of the -name
value (determining whether it creates a timeline of that name, or just seeks an ancestor timeline of that name). That feels less great.
A third possibility (sorry for the options) - do we actually need longhands here? This implies that we find it important to control the timeline name independently from the timeline axis. Is this indeed important? If not, we can simplify this significantly by just using a single property with a slightly larger grammar:
scroll-timeline: [ <timeline-name> && ancestor? && <axis-keywords>? ]#
A secondary issue: timeline names are <custom-ident>
right now. Is that a necessity? Making it <dashed-ident>
would be nice since it would avoid the grammar ever clashing in the future with new keywords.
Since we're not stuck with the prior properties yet, I think we should redesign slightly to better accommodate the model as we now understand it.
I think it's good to take this approach to thinking about the problem space, and I appreciate that you're doing it. :)
Btw, I will note that I just committed a bunch of editorial work to the PR for this proposal, based on the summary you made for me when you were commenting that it was confusing. I really liked that summary :) so I'm going to quote it here:
”So just making sure I understand: you can (a) create a timeline, with a name, and (b) provide a scroller that the timeline can reference. A name, once created, is visible to the element and its descendants. 'local' does both (a) and (b), attaching to the element's own scroller. 'defer' only does (a), creating the name. 'ancestor' only does (b), providing the scroller and attaching it to a specified name from an ancestor. Right?”
I'm not sure I succeeded in capturing your clarity, but I'm hoping the updated spec text is now easier to understand...
A third possibility (sorry for the options) - do we actually need longhands here?
Yes, actually, I think it's reasonable that someone might want to cascade the axis independently of the name: the name is more of a structural/markup thing, but the axis needs to correspond to how you're laying out the content of the scroller.
(That does bring up the question of if we can auto-determine the axis... filed https://github.com/w3c/csswg-drafts/issues/7749.)
scroll-timeline: foo; creates a foo timeline, but doesn't attach a scroll axis to it; it's assumed a descendant will do so
So I want to rewind you a bit: you've gone and grokked the underlying model we're using, and now you're trying to translate it into syntax.
But what I'd rather do is design this from a use-case-first perspective. And the most basic use cases are the ones that both declare and define a timeline on a single element. We should be optimizing for making that easy to do and easy to understand.
Detaching these two operations onto multiple elements is advanced stuff. We should make it comprehensible also, but it shouldn't be interfering with making the common case as simple and straightforward as possible.
The two operations the basic case is interested in is: name the timeline (and let the scoping fall out of that) and choose its axis, defaulting appropriately. Even if all you do is give a name, you get a timeline you can use!
Now for the complex case: we could add a keyword to one of these two properties (name, axis). But that complexifies what the property represents (name, axis) to the author. On the other hand, having properties that apply in some context and not others is all over CSS, so ignoring -attachment or ignoring -axis depending what you're trying to do is pretty normal.
And having a shorthand that lets you specify any relevant combination makes them all convenient to use.
So even though I agree with you that it's good to review the whole house, so to speak, and not just the extension; I think I like the current design better than the other ones you proposed. :)
But what about other possibilities? Going back to @bramus’s proposal, keeping scroll-timeline-name
and scroll-timeline-axis
as they are and introducing a name-scoping property like scroll-timeline-scope: <custom-ident>#
would make sense to me. The basic case is still basic, and an additional feature enables extended scoping. I think this is also very easy to use.
The main downside to it: it doesn't have the double-handshake that scroll-timeline-attachment
has. Currently timeline scopes nest by default, each hiding matching names on any ancestors, which works well to avoid name clashes with a nested component structure. But since Bramus’s proposal has an implied ancestor attachment, by adding -scope on an ancestor you can end up with multiple descendants all using the same name, and therefore conflicting in trying to attach to that scope root... which invalidates their connections with any descendants referring to their timeline in their otherwise happy little subtree bubble. :/
So it's slightly easier to use, but also slightly more likely to run into trouble. :)
A secondary issue: timeline names are
<custom-ident>
right now.
That's definitely a separate issue, and orthogonal to this one. :P
# But what about other possibilities? Going back to @bramus’s proposal, keeping
scroll-timeline-name
andscroll-timeline-axis
as they are and introducing a name-scoping property likescroll-timeline-scope: <custom-ident>#
would make sense to me. The basic case is still basic, and an additional feature enables extended scoping. I think this is also very easy to use.The main downside to it: it doesn't have the double-handshake that
scroll-timeline-attachment
has.
I personally don’t consider this double-handshake to be essential but assuming it is: what if we went for a hybrid approach of the current proposals?
scroll-timeline-name
and scroll-timeline-axis
as they are right now.scroll-timeline-scope: <custom-ident>#
(or scroll-timeline-root: <custom-ident>#
) to create a scope for a timeline with that name.scroll-timeline-attachment: <custom-ident>#
to attach to that scope.Note that I’ve adjusted scroll-timeline-attachment
here to require a <custom-ident>
instead of a keyword like ancestor
. Its value would link up the scroll-timeline-attachment
to the scroll-timeline-scope
, enabling the double opt-in and thereby addressing fantasai’s concern.
Code example:
.shared-parent {
scroll-timeline-scope: scroller; /* establish scope */
}
.shared-parent .scroller {
scroll-timeline: scroller inline;
scroll-timeline-attachment: scroller; /* Refers to the scroll-timeline-scope’s <custom-ident> */
}
.shared-parent .scroller + .subject {
animation-timeline: scroller; /* Looks for a scroll-timeline-name with that <custom-ident>. It can be found because scroll-timeline-scope on the .shared-parent has it linked. */
}
I think this is also in line with other specs, such as Anchor Positioning, where you have to explicitly refer to another thing by its <custom-ident>
(or <dashed-ident>
in the case of anchoring). That leaves no room for confusion or guessing, while allowing jumping over nodes.
A disadvantage though is that technically the scroll-timeline-scope
and scroll-timeline-name
can differ from each other as the former is linked from scroll-timeline-attachment
and the latter from animation-timeline
… unless it’s enforced in the spec that they all must be the same.
The scroll-timeline-attachment
could be included in the shorthand as follows …
scroll-timeline: [ <scroll-timeline-name> <scroll-timeline-attachment>? <scroll-timeline-axis>? ]#
… but it might feel pretty redundant to see a double <custom-ident>
then:
.shared-parent {
scroll-timeline-scope: scroller;
}
.shared-parent .scroller {
scroll-timeline: scroller scroller inline;
}
.shared-parent .scroller + .subject {
animation-timeline: scroller;
}
Rinse and repeat for view-timeline-*
.
A secondary issue: timeline names are
<custom-ident>
right now.
which invalidates their connections with any descendants referring to their timeline in their otherwise happy little subtree bubble. :/
I think each named timeline could still implicitly serve as a local root for that name as well as the selected timeline for the ancestor declared name if one existed. This would allow descendants to continue to be valid and use the same timeline in their happy subtree bubble.
The downside is that without the double handshake you would still be contaminating the ancestor timeline namespace with a timeline that was only intended to be used in its subtree - which a developer could avoid by making the subtree also an explicit timeline root for the name.
@bramus I think having three different naming properties is really confusing...
(#) @bramus I think having three different naming properties is really confusing...
It’s two (one for the scope and one for the timeline), but yeah I agree that it’s weird to have to use two names while only one should really be necessary.
Re-reading this thread with all possible options again, it looks like the scroll-timeline-root
proposal gained most traction. It had a few remarks about the double handshake which – I think – flackr addressed here.
Maybe it’s time to settle on it? From an author POV, I think this proposal strikes a good balance between keeping things simple while also allowing more complex setups without too much overhead.
(#) happy little subtree bubble
Can we make this an official term? 🙃
Detaching these two operations onto multiple elements is advanced stuff. We should make it comprehensible also, but it shouldn't be interfering with making the common case as simple and straightforward as possible.
Yeah, that's fine. My listing of shorthand possibilities was just obeying the usual maxim of "defaults match the longhand initial values", but as you're well aware that doesn't have to be the case. ^_^ I think it's completely reasonable for scroll-timeline: foo
to be equivalent to scroll-timeline-name: foo; scroll-timeline-axis: block;
, and requiring scroll-timeline: foo none;
to get the "just create, don't attach" behavior.
We could alternately fix this by adding an "auto" timeline-axis keyword and use that as the initial value, so then omitting it from the shorthand would do the right thing with the usual behavior.
I think each named timeline could still implicitly serve as a local root for that name as well as the selected timeline for the ancestor declared name if one existed. This would allow descendants to continue to be valid and use the same timeline in their happy subtree bubble.
Apologies @flackr, could you elaborate on this? I'm not completely sure what behavior you're suggesting here.
I think you're saying that if you, say, write scroll-timeline: foo block;
, this will automatically attach to an ancestor establishing a foo
timeline (but not attaching an axis) if one exists, and create its own foo
timeline if there's one (and attach the axis). And you can force the "create a timeline" behavior regardless of ancestors by setting -scope: foo
?
tldr: I really don't like the way that defer
works in the syntax; the ability of authors to write scroll-timeline: foo block defer;
seems genuinely confusing/contradictory. I have two suggestions for fixing it: one eliminates -attachment
entirely and folds the functionality into -name
and -axis
; the second just shifts the defer
keyword into -axis
so you can't specify it and an axis at the same time. Neither of these change the processing model at all from what the current spec specifies, or introduce any new concepts; they're purely syntax rearrangements.
Hm, giving it more thought, I'm still thinking that the current solution feels very artificial; even with the better editorial text, it feels like -attachment was designed and bolted onto an existing -name/-axis pair (which it effectively was) rather than being designed as part of it from the start. Depending on the value of this third property, one property changes behavior (creating a timeline, or looking up a timeline) and another is either used or ignored. This sort of mode-switching is not common in CSS, and it's weird for it to be cascaded separately, too.
So, like, the the fundamental deal is still that you have two operations (define a timeline scope, and attach a scrolling axis to a timeline in scope) and want to do either or both of them (and when doing both, you want them linked together implicitly). When you're doing both, the current split of properties already maps perfectly to the two tasks: -name
defines the scope, and -axis
attaches the scrolling axis.
Ideally we'd be able to handle the two single-operation scenarios just by modifying the necessary property. Here's one more sketch that does precisely that, as simply as possible:
scroll-timeline-name: none | [ <custom-ident> [ root | ancestor ]? ]#
scroll-timeline-axis: [ block | ... | defer ]#
-name
can either be in root
mode (creates a timeline of the given name) or ancestor
mode (looks for an ancestor of the given name). If omitted, defaults to root
.
-axis
either attaches the given scrolling axis to the timeline selected by -name
, or does nothing (if defer
). If omitted, defaults to block
.
This allows the useless combination of scroll-timeline: foo ancestor defer;
, which does nothing. But I think this is less confusing than the current spec's scroll-timeline: foo block defer;
possibility, which is genuinely unclear in whether it attaches the attaches the block scrolling axis to foo
or defers it.
I think the cascade behavior is slightly more reasonable, too.
(We could eliminate the possibility of a useless/confusing combo entirely, by instead moving the ancestor
keyword down to -axis
, with a grammar of [ [ block | ... ] [ local | ancestor ]? | defer]
, but I think that's a little more confusing in different ways, since the -axis
value would be changing the interpretation of -name
. It also makes it look like you're referring to the block axis of an ancestor, rather than using your block axis for an ancestor.)
Or here, one final possibility if we are really attached to -attachment
and want a minimal edit from the current spec. The thing that's really getting my goat here is that the defer
value causes us to ignore -axis
, but we can still specify it.
scroll-timeline-name: <custom-ident>#;
scroll-timeline-axis: [ block | ... | defer]#;
scroll-timeline-attachment: [ local | ancestor ]#
All values are treated exactly as the spec currently defines, it's just that the defer
keyword is moved to the -axis
property, which prevents you from specifying it alongside an axis in the shorthand. This also means that now -attachment
merely changes the interpretation of -name
, which is a little less unusual for CSS things.
This, similarly, allows the no-op shorthand value of foo defer ancestor
, but as I said above I think that's better than the current conflicting value of foo block defer
.
Oh, and either of these are consistent with the additional -inset
property you need for view timelines. You can write view-timeline: foo defer 10%;
which makes the %s useless and ignored, but it's clearer that they are ignored, since you're specifically saying there's no axis to attach to. (Or we can specifically define the shorthand to disallow this, too, which is probably good.)
@tabatkins' ideas here look good to me until we throw view-timeline-inset
into the mix ... this seems to bring us back to "property is annoyingly just ignored" without a fundamental improvement over the existing -attachment
?
but it's clearer that they are ignored
OK, maybe some improvement ...
view-timeline: foo defer 10%
Minor note: view-timeline
actually doesn't accept view-timeline-inset
at all.
this seems to bring us back to "property is annoyingly just ignored" without a fundamental improvement over the existing -attachment?
It's not at all unusual for a property to turn another property on or off (even several, if they're all linked), by making whatever those properties are doing irrelevant. (display
is the most obvious example, turning on or off a bunch of layout-specific properties.) We also have a number of examples of two closely-linked properties affecting each other's interpretation (so the local | ancestor
keywords in -attachment
changing the -name
value from creation to lookup isn't too unusual), tho we do limit this sort of thing when possible, and instead try to localize behaviors inside a single property.
What's unusual is one property affecting several disparate properties in distinct ways, so they get interpreted significantly differently. That's what the current spec for -attachment
does - depending on its value, it turns -axis
on or off and twiddles the creation/lookup behavior of -name
, and enables three of the four possible combinations of those two distinct behaviors. That's a step further of complexity/indirection than we usually allow for in property designs; we usually try to keep things as linear as possible in their linkages.
Minor note: view-timeline actually doesn't accept view-timeline-inset at all.
Oh funky, why not?
Is it possible, perhaps, to have an attached scroller without it explicitly declaring it's providing attachment upwards to an ancestor scope?
Borrowing from @flackr's example, with @tabatkins' modification we should have something like:
<style>
#parent {
scroll-timeline-name: scroller;
scroll-timeline-axis: defer;
}
#scroller {
scroll-timeline-name: scroller;
scroll-timeline-axis: block;
}
#animated {
animation-timeline: scroller;
}
</style>
<div id="parent">
<div>
<div id="animated"></div>
</div>
<div id="scroller"></div>
</div>
And then scroller
should be able to provide attachment both upwards and downwards, and we don't have any awkward specifiable options.
Right?
@tabatkins OK, can we boil this down to some proposed resolution?
defer
to -axis.?
Minor note: view-timeline actually doesn't accept view-timeline-inset at all.
Oh funky, why not?
No idea, ask @fantasai. :-)
Is it possible, perhaps, to have an attached scroller without it explicitly declaring it's providing attachment upwards to an ancestor scope?
My impression is that we don't want to allow the outer timeline to connect to the inner timeline without the inner timeline's explicit consent.
@andruud you mean @fantasai's comment:
...having a handshake like this reduces conflicts like some other element defining timeline with the same name and expecting it to bind locally. It also allows for recursion, which re-using the same property doesn't.
?
I guess I'm less concerned with this, having clashing names.
Since scroll-timeline
is a list, authors could specify another timeline for local attachment with a different name on the same element.
And if a clashing name happens by mistake then I expect things to break, isn't it why it's called -ident
after all? I mean, IMHO, I expect that using same identity twice should fail, instead of allowing an explicit mechanism that prevents it.
I think each named timeline could still implicitly serve as a local root for that name as well as the selected timeline for the ancestor declared name if one existed. This would allow descendants to continue to be valid and use the same timeline in their happy subtree bubble.
Apologies @flackr, could you elaborate on this? I'm not completely sure what behavior you're suggesting here.
I think you're saying that if you, say, write
scroll-timeline: foo block;
, this will automatically attach to an ancestor establishing afoo
timeline (but not attaching an axis) if one exists, and create its ownfoo
timeline if there's one (and attach the axis). And you can force the "create a timeline" behavior regardless of ancestors by setting-scope: foo
?
Almost. I'm saying that *-timeline-root: foo
only establishes the name foo. *-timeline-name: foo
attaches a scroll/view timeline for the current element as scroller/subject to the nearest ancestor *-timeline-root: foo
, and also makes that timeline-name available to itself / any descendant elements referring to that animation-timeline
.
This means that for the happy little subtree case:
<style>
unhappy-root {
/* Establishes the name foo, none of the other view-timeline-* properties matter for foo */
view-timeline-root: foo;
/* Could even provide a view-timeline for this subject with a different name which would use the axis: */
view-timeline: bar block;
}
happy-subtree {
view-timeline: foo block;
}
.observer {
animation: frames;
animation-timeline: foo;
}
</style>
<unhappy-root>
<happy-subtree> <!-- defines view-timeline foo for this subtree AND adds it to the unhappy-root foo timeline -->
<div class="observer"></div> <!-- happily uses its parent timeline -->
</happy-subtree>
<happy-subtree> <!-- defines view-timeline foo for this subtree AND adds it to the unhappy-root foo timeline -->
<div class="observer"></div> <!-- happily uses its parent timeline -->
</happy-subtree>
<div class="observer"></div> <!-- unhappy - has two timelines associated with the root so doesn't run -->
</unhappy-root>
@flackr that can still happen with current syntax, attaching multiple timelines with ancestor
to same root with defer
.
IIUC, the only difference with current model is that it requires specifying ancestor
on a declared timeline, which allows that second handshake.
While it's much safer, I find it somewhat of an overkill, at least for this feature, for protecting against clashes.
I find the single handshake - opt-in for hoisting a name - quite satisfying for that purpose. That's, like, my opinion.
Also, as has I think been mentioned I find it a bit odd to add defer
to axis
when that is just one of the properties of the declared timeline. E.g. view timelines also have inset
which will have the same oddity.
It feels like perhaps we're at the point of enumerating the options and bringing it up for discussion as a group to resolve on an outcome.
The options seem to be (please correct me if I've gotten some details wrong):
scroll-timeline-attachment: ancestor | defer | local
. When declaring a name allows the name to be defined as attaching to an ancestor with defer
or define a name for descendants to map to or declare a local timeline.scroll-timeline-root: <name>
. Declares a root for a given scroll timeline name. A timeline with a given name in the root's subtree would be accessible for reference from any other element under that root. The root could even declare a different timeline name locally.scroll-timeline-axis: defer | block | inline
. Makes it clear that the deferred name does not use its axis. However, we have the same oddity with view-timeline-inset which is ignored on the root.A few common details independent of the above option:
animation.timeline
) with the attached timeline instance, which I think is a simpler mental model for developers.After thinking through these, I find myself leaning towards the scroll-timeline-root
/ view-timeline-root
direction though all of the options solve the use cases and I don't think any of them have particularly tricky edge cases. I.e. I've shown that we can make subtrees continue to work even without the double-handshake by having them also define their timeline with their name locally - though still continuing to make the root instance attach to them.
Also, as has I think been mentioned I find it a bit odd to add defer to axis when that is just one of the properties of the declared timeline. E.g. view timelines also have inset which will have the same oddity.
My issue (which has taken me a while to fully suss out from my own disquiet, sorry) is that the current design for -attachment
is mixing responsibilities more than we usually do.
-inset
is an elaboration on -axis
, so it's fairly normal for it to simply not work when -axis
isn't defining an axis. -attachment
isn't an over-property of -name
and -axis
, tho, so it's less usual for it to have this sort of effect. It's doing two separate things right now.
If defer
is moved off to -axis
, tho, then -attachment
becomes a paired property with -name
, controlling whether you create (and use) a new timeline on the element, or search for an existing timeline on an ancestor. The lines of responsibility become cleaner: -name
and -attachment
select a timeline, and -axis
(and -view
for view timelines) attach controlling mechanisms to the selected timeline.
There are multiple ways to achieve this cleaner division, tho - using -root
to force timeline creation (and letting -name
auto-search, and just create-by-default if the search fails) is another way. It does mean slightly more repetition, but the overall weight is essentially identical.
I think @bramus's idea of a -root
syntax that takes a name is more complex than we need, tho. Here's a simpler form:
*-timeline-root: [ auto | self | ancestor ]#
*-timeline-name: [ <custom-ident> ]#
*-timeline-axis: [ block | inline | ... | defer ]#
-root
defines where the timeline root is to be found: self
creates a fresh timeline on the element, ancestor
searches for an unattached timeline on an ancestor, auto
tries to search and instead creates if the search fails.
-name
and -axis
work as already explained. In particular, -axis: defer
means the element doesn't attach anything to the timeline selected by -name
/-root
, while the other values do attach something.
(Yes, this is just renaming -attachment
and its values to -root
. The -attachment
behavior isn't bad once you remove defer
from it so it can focus on one thing.)
OK, can we boil this down to some proposed resolution?
Move defer to -axis.
?
I'm fine with that.
Something Bramuslike could work in my opinon. Fundamentally this is about declaring something in one place, and defining it in another place. Using two different properties for this seems reasonable to me. How about this tweak:
scroll-timeline-root: [ none | <custom-ident> ]#
scroll-timeline-name: <custom-ident>#
scroll-timeline-axis: [ block | inline | x | y ]#
scroll-timeline: [ <custom-ident> [ <'scroll-timeline-axis'> || ancestor ]? ]#
scroll-timeline-root
declares the named timeline.scroll-timeline-name
, -axis
(/ -inset
) defines the named timeline.scroll-timeline
shorthand by default will expand the <custom-ident>
to both -name
and -root
properties, unless:ancestor
keyword is provided, which instead expands the <custom-ident>
to -name
only, and expands -root
to none
.Note that no longhand accepts ancestor
nor defer
.
Examples:
scroll-timeline:foo block; => scroll-timeline-root:foo; scroll-timeline-name:foo; scroll-timeline-axis:block;
scroll-timeline:foo block ancestor; => scroll-timeline-root:none; scroll-timeline-name:foo; scroll-timeline-axis:block;
scroll-timeline-root:foo
=> Effectively scroll-timeline:name defer
in current spec terms.~If we need the "behave as local if no ancestor is found"-behavior then we could do scroll-timeline-root:auto
instead of none
.~
EDIT: If we need the "behave as local if no ancestor is found"-behavior, then a given timeline definition could create an implicit local declaration (i.e. root) if no matching declaration is found in the ancestors.
From #7047, it might be worth looking into the ability to split the declaration of a timeline (together with its scoping) from its actual attachment to a scroll container.
This would allow authors, for example, to declare a name on a subtree and make it available to all descendants of that subtree, and attach it to a scroll container that is a descendant within the subtree. (At the top level, declaring the name on the root element would make it global.)
Maybe something like
scroll-timeline-attachment: local | defer | ancestor | closest
where:local
has the current behavior of binding the name to this element’s scroll containerdefer
declares and scopes the name, but does not bind it to a scroll containerancestor
looks up the ancestor chain for a matching timeline name and attaches to that instance; failing a match, declares it locallyclosest
looks back up the tree including previous siblings for a matching timeline name (same lookup as animation-timeline), and attaches to the first matching instance; failing a match, declares it locally