w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.52k stars 673 forks source link

[scroll-animations] Broader scope of scroll timelines #7759

Closed fantasai closed 1 year ago

fantasai commented 2 years ago

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:

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed scope of named timelines.

The full IRC log of that discussion <TabAtkins> Topic: scope of named timelines
<TabAtkins> github: https://github.com/w3c/csswg-drafts/issues/7047
<fantasai> github: https://github.com/w3c/csswg-drafts/issues/7047#issuecomment-1239820755
<TabAtkins> fantasai: we talked about the scop eand came up with a default answer
<TabAtkins> fantasai: also what we might do to give power to people who want to do something more far-ranging than just "prev sibling" or "ancestor"
<TabAtkins> fantasai: so i came up with a suggestion to declare a timeline name but not attach it to a scroll container, and then attach it to one in the sibling/descendants
<TabAtkins> fantasai: suggestion is a property called scroll-timeline-attachment
<TabAtkins> 'local' is the default, binds to the element you're on
<TabAtkins> 'defer' will declare the name, but attach it to somethign else
<TabAtkins> fantasai: And another element can say "attach me to the timeline" of a given name
<TabAtkins> fantasai: so if i want a global scope
<TabAtkins> fantasai: can se the scroll-timeline-name:foo on the root , and scroll-timeline-attachment:defer
<TabAtkins> then on something else in the tree set scroll-timeline-name:foo and scroll-timeline-attachment: ancestor
<TabAtkins> fantasai: and it'll attach to that timeline scoped to the root
<TabAtkins> fantasai: i'm ambivalent about putting this in current spec, but wanted opinions on idea
<flackr> q+
<TabAtkins> zakim, open queue
<Zakim> ok, TabAtkins, the speaker queue is open
<TabAtkins> flackr: two thoughts
<TabAtkins> flackr: if you can name a timeline that's deferred, i don't think there's any need for the ancestor selection anymore, bc you could just name that timeline?
<TabAtkins> fantasai: defer says you're planning to attach the name to something else
<TabAtkins> fantasai: but if the element is also a scroll container
<TabAtkins> fantasai: it doesn't actually bind the timeline to its own scroller
<TabAtkins> flackr: okay so i misunderstood, 'ancestor' is the name lookup
<TabAtkins> flackr: yeah i proposed something similar earlier
<TabAtkins> flackr: so two, we havne't explored this idea too far yet, t5o see if there's a need for it
<TabAtkins> flackr: i guess it sovles every use-case since you can do global timelines
<TabAtkins> flackr: but i'm interested to see in practice how often people need these other scopes
<TabAtkins> flackr: anyway if we do need this power, i think this is a reasonable approach
<TabAtkins> astearns: maybe this could be an open issue to put in the next level if needed?
<TabAtkins> fantasai: yup
<TabAtkins> astearns: unless it would be good to put in the current draft to get more eyes on it, and punt if needed later
<TabAtkins> fantasai: i defer that judgement to flackr
<TabAtkins> flackr: I think we can add this later
<fantasai> github: https://github.com/w3c/csswg-drafts/issues/7759
flackr commented 2 years ago

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>
fantasai commented 1 year ago

Agenda+ to discuss @flackr's suggestion to narrow the default scope to ancestors.

flackr commented 1 year ago

@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.

andruud commented 1 year ago

@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. :-)

css-meeting-bot commented 1 year ago

The CSS Working Group just discussed [scroll-animations] Broader scope of scroll timelines, and agreed to the following:

The full IRC log of that discussion <emilio> flackr: we previously deferred this, but it came up that this could actually simplify timeline-name lookup
<emilio> ... where if you can define timeline names on ancestors we don't need sibling lookups
<emilio> ... this is probably architecturally simpler and already have a reasonable API for this
<emilio> fantasai: to summarize there's three options: do nothing (lookup would be ancestors and prev-siblings), adapt this explicit exposing mechanism proposed in the issue and narrow default to ancestors only, or narrow it to ancestors only for now but acknowledging we can expand in the future
<emilio> ... [describes proposal]
<Rossen_> q?
<emilio> ... so you can do attachment with my timeline name or saying "I'm an ancestor and I can declare a name etc"
<emilio> q+
<emilio> flackr: there's some hierarchies where this defer attachment is required for
<emilio> ... and it nicely generalizes
<fantasai> scribe+
<emilio> ... so I'd like to adapt it including the stricter scoping
<Rossen_> ack emilio
<fantasai> emilio: Just wanted to confirm that the proposal included dropping the sibling lookup?
<fantasai> emilio: otherwise sounds good to me
<emilio> flackr: yeah I propose dropping sibling and having deferred attachment drop those
<emilio> emilio: sgtm
<emilio> Rossen_: that's option 3 right?
<emilio> fantasai: yes
<bramus> q+
<fantasai> s/3/2/
<emilio> bramus: what's the default mechanism here/
<Rossen_> ack bramus
<emilio> fantasai: it'd look up the ancestors
<emilio> not siblings
<emilio> bramus: so if you want to include preceding siblings you'd have to define the attachment somewhere?
<emilio> fantasai: you'd declare a name in the common ancestor and the scroller would attach to that name
<emilio> flackr: my comment from sept. 7 has an example of this
<Rossen_> q?
<fantasai> PROPOSAL: Reduce default scoping to ancestors only, add scroll-timeline-attachment as proposed in the issue
<fantasai> s/proposed/described/
<emilio> RESOLVED: Reduce default scoping to ancestors only, add scroll-timeline-attachment as proposed in the issue
bramus commented 1 year ago

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:

  1. Find a common ancestor and duplicate the scroll-timeline-name there
  2. Set scroll-timeline-attachment to defer on that ancestor
  3. Set scroll-timeline-attachment to ancestor on the scroller

This 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:

  1. Find a common ancestor and set the scroll-timeline-root there

Reworking 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.

flackr commented 1 year ago

@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:

  1. Handling multiple timelines attempting to attach to the same deferred timeline is complicated. Can we consider this an error?

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.

  1. 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.

bramus commented 1 year ago

So defining scroll-timeline-name in the absence of a root is an implicit root right?

Correct.

  1. 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.

  1. 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?

bramus commented 1 year ago

@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.

flackr commented 1 year ago

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.

bramus commented 1 year ago

Oh yeah, of course – I was too focused on animating the subject itself 🤦‍♂️. Makes sense to add it.

fantasai commented 1 year ago

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:

bramus commented 1 year ago

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:

  1. To be clear, the syntax for the scroll-timeline shorthand would then become this?
    [ <'scroll-timeline-name'> [<'scroll-timeline-axis'> || <'scroll-timeline-attachment'>]? ]#
  2. Linking back to @andruud’s question, there could also be a view-timeline-attachment property for View Progress Timelines?
    [ <'view-timeline-name'> [<'view-timeline-axis'> || <'view-timeline-attachment'>]? ]#
  3. The value of 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.
flackr commented 1 year ago

@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?

flackr commented 1 year ago

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.

bramus commented 1 year ago

Sounds perfect!

andruud commented 1 year ago

@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.

flackr commented 1 year ago

@andruud That's a good question. I have a few thoughts:

  1. Rather than thinking of the "timeline" as dynamic we could think of the subject/source as dynamic. I hate to propose yet more properties but in the interest of finding the best ergonomics here we go. We could have something like scroll-timeline-source: <name> and view-timeline-subject: <name> which set the current element as the source/subject for the named scroll/view timeline.
  2. If we assume the root is just a name reservation (which had been my thinking so far), we could try to craft the 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.
  3. Go back to having a separate property to explicitly declare the root as @bramus suggested above. I just want to be sure I understand @fantasai's one concern that it doesn't allow for recursion. This is that unless you set scroll-timeline-root again any scroll timeline doesn't become an implicit root?
andruud commented 1 year ago

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.

flackr commented 1 year ago

@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.

fantasai commented 1 year ago

@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?

bramus commented 1 year ago

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.

andruud commented 1 year ago

@fantasai @flackr @bramus I attempted to spec this here: https://github.com/w3c/csswg-drafts/pull/8680

fantasai commented 1 year ago

@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 commented 1 year ago

@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.

bramus commented 1 year ago

@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.

flackr commented 1 year ago

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.

bramus commented 1 year ago

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.

tabatkins commented 1 year ago

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:

  1. Define a (timeline name, element scope) tuple, to establish what elements can use a given timeline.
  2. Attach a particular scroller's axis to a particular timeline name.

My syntax proposal borrows from Bramus's:

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:


We could eliminate the dead possibility by instead specifying ancestor in scroll-timeline-axis instead. That is:

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:


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.

fantasai commented 1 year ago

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

bramus commented 1 year ago

# 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.

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?

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-*.

andruud commented 1 year ago

A secondary issue: timeline names are <custom-ident> right now.

https://github.com/w3c/csswg-drafts/issues/8746

flackr commented 1 year ago

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.

fantasai commented 1 year ago

@bramus I think having three different naming properties is really confusing...

bramus commented 1 year ago

(#) @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? 🙃

tabatkins commented 1 year ago

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?

tabatkins commented 1 year ago

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.

tabatkins commented 1 year ago

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.)

andruud commented 1 year ago

@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.

tabatkins commented 1 year ago

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?

ydaniv commented 1 year ago

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?

andruud commented 1 year ago

@tabatkins OK, can we boil this down to some proposed resolution?

?

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.

ydaniv commented 1 year ago

@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.

flackr commented 1 year ago

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?

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>
ydaniv commented 1 year ago

@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.

flackr commented 1 year ago

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):

  1. 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.
  2. 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.
  3. 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:

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.

tabatkins commented 1 year ago

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.)

tabatkins commented 1 year ago

OK, can we boil this down to some proposed resolution?

Move defer to -axis.

?

I'm fine with that.

andruud commented 1 year ago

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 ]? ]#

Note that no longhand accepts ancestor nor defer.

Examples:

~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.