Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-anchor-position-1] Better reusability of anchor names #9045

Closed
tigrr opened this issue Jul 8, 2023 · 11 comments
Closed

[css-anchor-position-1] Better reusability of anchor names #9045

tigrr opened this issue Jul 8, 2023 · 11 comments

Comments

@tigrr
Copy link

tigrr commented Jul 8, 2023

Spec: https://drafts.csswg.org/css-anchor-position-1/

The anchor name is currently defined to be a tree scoped reference (i. e. unique for the entire document). This makes it difficult to reuse.

For example tooltips on elements are often implemented using a class or an aria-label attribute together with a pseudo-element holding text from that attribute.

In this fiddle you can see that all 3 of the tooltips are positioned on the first element defining anchor-name, not on their closest ancestor as one may have expected.

Anchor names being unique throughout the document forces us to explicitly name an anchor for each and every one of those elements with tooltips. And there may be hundreds of them on the page.

If anchor-name was not defined as unique but was scoped instead, similar to container queries (container-name property), every element with anchor-name would create its own anchor. But then of course what to do with anchors that are not ancestors of an anchored element?

Perhaps it would be possible to introduce a modifier in the anchor selecting syntax, which will select not the first instance of an element with the given anchor name but the closest ancestor?

So, instead of:

anchor-default: --tooltip;

we could write:

anchor-default: closest --tooltip;

And instead of:

top: anchor(--tooltip);

we could write:

top: anchor(closest --tooltip);
@kizu
Copy link
Member

kizu commented Jul 19, 2023

One thing we can currently do to scope the anchor names is to use position: relative on a wrapper: https://jsfiddle.net/3fpukxan/

Interestingly, this does not work if we add the position: relative to the element that has anchor-name itself: https://jsfiddle.net/3fpukxan/1/ — cc @tabatkins — is this expected? As an author, I would expect the position: relative with an anchor-name to continue providing this name to any nested elements that could want to use it as an anchor.

However, as I did mention in #8588 (this issue could be considered a duplicate of it, maybe?), I don't like this implicit behavior of position, with implicit scopes. While it can be useful, as an author, I would want more control over when a scope is created.

Playing with scroll-driven animations, I did appreciate how it had the ability to define explicit scopes via timeline-scope property.

So, my proposal: what if we had an anchor-scope one?

I imagine it work like this:

Name: anchor-scope
Value: auto | all | none | <dashed-ident>#
Initial: auto
Applies to: all elements that generate a principal box

The anchor-scope property declares the anchor-name lookup scope on this element's subtree, including the element itself. This allows any properties on the elements in the subtree to only consider elements inside this scope as the acceptable anchor elements. Values are defined as follows:

all: Declares the scope for all explicit anchor elements.

none: The property has no effect.

auto: Resolves to all if this element is a containing block, and to none otherwise.

<dashed-ident>: Specifies the anchor name that would be scoped by this element. All other anchor names could escape this scope.

(This is a very quick draft; I imagine there can be other use cases to consider, like “do we want to have an inversion of the <dashed-ident>, like if we want to have all but let some specific anchor names escape it?”, but I hope the idea is clear.)

@xiaochengh
Copy link
Contributor

The anchor name is currently defined to be a tree scoped reference (i. e. unique for the entire document). This makes it difficult to reuse.

While it's a tree-scoped reference, it doesn't have to be unique for the document. It's scoped to the containing block of the anchor-positioned element.

That being said, this scope is still the entire tree scope for popovers, so I can see there's still some difficulty here.

Interestingly, this does not work if we add the position: relative to the element that has anchor-name itself: https://jsfiddle.net/3fpukxan/1/

This is expected. We can't use the anchor name on the containing block itself.

Btw, if you want it anchored to the containing block, isn't it just the good old positioned layout (without anchor())?

@tabatkins
Copy link
Member

tabatkins commented Jul 20, 2023

(i. e. unique for the entire document).

Yeah, as @xiaochengh said, this is incorrect. Tree-scoped-ness is just about how the names and references work with shadow trees, it says nothing about uniqueness.

But also, yeah, the behavior for multiple valid anchors with the same name won't actually help you here, since all of the anchors are in scope for all of the tooltips here.

Btw, if you want it anchored to the containing block, isn't it just the good old positioned layout (without anchor())?

In theory, tho you'll be limited to what plain positioning can do. At least in this case, putting the abspos "below the element" is easy with a top: 100%;, but there might be other things you want to do that require anchor positioning. For example, top: anchor(bottom); bottom: 0; would let the box stretch from the bottom of the anchor to the bottom of the screen, and you can't do that here.

We can't use the anchor name on the containing block itself.

I forget why this is the case - is it because the positioned element can trigger scrollbars on the containing block, and thus we don't have the "laid out strictly after" condition, @xiaochengh ?

[anchor-scope proposal]

I suspect some simple way to say "look for closest ancestor with the given anchor-name" (rather than "last element in the page with the given anchor-name") would address the common case here. Probably can bake it into the <anchor-element> grammar, so anchor-default: closest --tooltip; or top: anchor(closest --tooltip bottom); would work for these cases.

@tabatkins
Copy link
Member

Hm, re: "closest" tho, we probably want to allow both for "positioned element is inside of its anchor" and "positioned element is right after its anchor". I think both markup patterns are reasonable, depending on what you're doing.

It's probably okay to just conflate the two, and have the search first look for preceding siblings, then look for ancestors?

@kizu
Copy link
Member

kizu commented Jul 20, 2023

@xiaochengh
Btw, if you want it anchored to the containing block, isn't it just the good old positioned layout (without anchor())?

Almost, but we couldn't use the anchor-fallback. Also, I can see cases where the anchor-name and position: relative could come from different sources, and when looking at the code, I would expect the position: relative to not break this case when both would be combined on one element.

@tabatkins is it because the positioned element can trigger scrollbars on the containing block, and thus we don't have the "laid out strictly after" condition,

Ah, I see how the scrollbars on the containing block could prevent this from working, yes. If the scrollbars are the only case, I wonder if there is a way to solve this by assuming that they're absent only for this particular case? Not ideal, and I wish there could be a better solution, but I find having this kind of an exception better than requiring adding an extra wrapper or moving the element outside its container (this won't be always possible, like when the container is a li, td, when CSS relies on nth-child etc).

Hm, re: "closest" tho, we probably want to allow both for "positioned element is inside of its anchor" and "positioned element is right after its anchor". I think both markup patterns are reasonable, depending on what you're doing.

It's probably okay to just conflate the two, and have the search first look for preceding siblings, then look for ancestors?

Thinking more about this — I like the idea of a closest, though agree that we'd need to think more about if we'd want to conflate siblings/ancestors. I would need to go through my use cases and see how they would behave with either.

@xiaochengh
Copy link
Contributor

We can't use the anchor name on the containing block itself.

I forget why this is the case - is it because the positioned element can trigger scrollbars on the containing block, and thus we don't have the "laid out strictly after" condition, @xiaochengh ?

I don't rememeber exactly. But we already the same issue with the plain positioned layout, e.g.

<style>
body {
  height: 0;
  overflow: auto;
}

#target {
  position: absolute;
  width: 100px;
  height: 100px;
  right: 0;
  background: lime;
  top: calc(100vh - 10px); /* triggers vertical scrollbar to the right*/
}
</style>
<body>
<div id=target></div>
</body>

In the end, #target gets right-aligned to the left edge of the scrollbar to the right. It appears that when scrollbars are triggered, we rerun layout with a modified containing block. I haven't looked into the spec/impl to confirm this though.

It seems that we can do the same when anchoring to CB. Just some more layout passes, what can possibly go wrong?

(famous last words)

@tigrr
Copy link
Author

tigrr commented Jul 21, 2023

Defining closest as either ancestor or preceding sibling sounds a bit arbitrary to me.
We could define closest as the last element before this one in the document order. This would include any elements for which the opening tag appears before this element, including ancestors previous siblings and their decedents. I don't think this would be an ideal solution tough. It can produce some unexpected results, such as having the anchor somewhere deep inside the preceding sibling.
Instead of joining the ancestor and preceding sibling selection in one keyword, why not split:

  • closest - for ancestor
  • preceding or previous - for any preceding sibling
  • following or next - for any following sibling
  • maybe even sibling - for any sibling.
    To insure familiarity and predictability closest will act in accordance with Element.closest() method. The above is also similar to how tree traversing works in jQuery.

P. S. All the containing block stuff was pretty confusing to me. Sorry 😅

@xiaochengh
Copy link
Contributor

The Anchor Positioning Exploration doc also includes an anchor-scope proposal. Let me put it here for reference:

anchor-scope: none | all | <dashed-ident>#

The anchor-scope property allows positioning anchor name lookup to be scoped, much the same way timeline-scope allows timeline name lookup to be scoped.

@bramus
Copy link
Contributor

bramus commented Aug 30, 2023

Note that the mentioned timeline-scope property currently does not accept a value of all.

timeline-scope: none | <dashed-ident>#

It is currently being proposed to add all to its allowed values – see #9158. This would bring its syntax in sync with the anchor-scope from the proposal.

@xiaochengh
Copy link
Contributor

Re @tigrr #9045 (comment):

The closest specifier doesn't seem to work with the current model.

Anchor Positioning is based on Absolute Positioning. When laying out the anchored element, the context is its containing block, which is its parent in the box tree. When querying an anchor name, we are not supposed to see anything outside the containing block. In other words, we are not supposed to see any ancestor other than the parent node.

Also, the next/following specifier may violate the acceptable anchor element criteria, which require that an element must be laid out completely after its anchor (and therefore, the anchor relationship in the entire tree is acyclic), so we are not always able to anchor to a following sibling.

Btw, the current spec doesn't even allow using the containing block as the anchor element. So the only anchors we can use are siblings and their descendants.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-anchor-position-1] Better reusability of anchor names, and agreed to the following:

  • RESOLVED: Add an `anchor-scope` property
The full IRC log of that discussion <emeyer> TabAtkins: It was brought up that right now, per spec, anchor names are whole-document visible
<fantasai> proposal -> https://github.com//issues/9045#issuecomment-1698431258
<emeyer> …They don’t have to be unique, and there are rules to deal with non-unique, but they’re all document-global
<emeyer> …So it’s hard to make a component that has self-contained visibility
<emeyer> …We’d like to fix this; there are a number of ways to do this
<emeyer> …We think we’d like to have an anchor-scope property
<emeyer> …You’d designate a container that scopes internal names or all names
<emeyer> …You can make a component, use a well-chosen name, and abspos will find the correct nearby anchor without grabbing something else on the page
<emeyer> fantasai: This was one of the things outlined when looking through the proposal back in July, so I think it makes sense to follow timeline scope
<emeyer> astearns: Do we want to create timeline-scope, anchor-scope, whatever-else-scope, or can we create an ident-scope?
<bramus> +1 to what fantasai said
<emeyer> TabAtkins: No. It’s hard and namespaces clash
<bramus> there’s a separate issue for `timeline-scope`: https://github.com//issues/9158
<emeyer> …You could allow naming the thing you’re scoping, but then you end up with additive scope clashes
<emeyer> astearns: Works for me
<fantasai> +1
<emeyer> bramus: +1 to fantasai about keeping timeline-scope separate
<emeyer> astearns: Proposal is to add `anchor-scope`
<emeyer> TabAtkins: Yes, which works like `tineline-scope`
<emeyer> astearns: We may want a central place to document all of this
<emeyer> astearns: Any objections?
<emeyer> (silence)
<emeyer> RESOLVED: Add an `anchor-scope` property
<vmpstr> vmpstr
<emeyer> vmpstr: Will style containment still scope acnhors independent of this property?
<emeyer> fantasai: Yes
<emeyer> astearns: It would win, I believe
<emeyer> fantasai: Yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Friday Afternoon
Development

No branches or pull requests

6 participants