Closed tabatkins closed 5 months ago
I think JUST add display:none could be jarring as anchors scroll in and out of the visible content.
From an Open UI perspective for popup=hint
, it might be better to trigger a hide event when anchors scroll out of the visible content, rather than hiding and reshowing.
It might be hard to guess what a good default would be for this, hiding is not a bad idea.
Another thought is that maybe sometimes people will want the anchored element to be clipped by the scroll container rather than hidden?
I think JUST add display:none could be jarring as anchors scroll in and out of the visible content.
It could be, or could not be, depending! If your anchors purposely hang out outside of the scroller, tho, you've gotta do something when the anchor is out of the viewport.
From an Open UI perspective for popup=hint, it might be better to trigger a hide event when anchors scroll out of the visible content, rather than hiding and reshowing.
I haven't dug deeply enough into OpenUI - can you give me a pointer to hide event stuff?
Another thought is that maybe sometimes people will want the anchored element to be clipped by the scroll container rather than hidden?
You can achieve this by putting the positioned element in the same scroller as the anchor.
Just saw the blog on this and was concerned about this, watching them overlayed outside the container. My custom anchor system uses IntersectionObserver
:
this
is the the anchor. If the anchor is less than 50% visible, I close the tooltip. Automatic handling is neat, but I think IntersectionObserver
would handle it. Now if we're saying over CSS, then that's something else...
Edit: Newer commit
I have a proposal that's built on top of #8724, which if resolved will allow us to specify a bounding box (not just the CB in positioned layout) for position fallback.
Then we can have a new property that controls what to do when the anchored element ends up overflowing that bounding box:
position-fallback-overflow: clip | visible
clip
means the anchored element should be clipped by the bounding box. visible
means no such clipping.
The initial value is clip
.
Discussion:
I think using the position fallback bounding box is more natural than checking whether the anchor is off screen (or off any box). And it doesn't have any issue with multiple anchors.
Clipping seems to provide a better/smoother UX than hiding & showing. User probably don't want to see the element suddenly disappear when something is just slightly off screen.
The UX can be even smoother if we have #8200
Clipping seems more friendly to the rendering pipeline than hiding/showing, because we are just introducing a clip box.
Agreed that it should be activated by default. But it might be just safer to also provide an opt-out. (and it also means I don't need to massively rewrite all the existing WPT)
I don't think it should fire any event on its own. IntersectionObserver
should be good enough, at least for now.
This is an excellent argument, and a great idea. Yeah, I think being able to clip to the box that you're using for overflow bounds sounds great.
Not sure we should default to clip
, tho - allowing the element to extend out of the element by default seems fine. Like, a tooltip should be able to poke out a bit. In particular, I think that the behavior if you get it wrong is better if we default to visible
: if you mean to clip and forget it's probably pretty obvious in common cases (you'll have things floating in space randomly on the page), while if we default to clip and you mean to use visible
that's only obvious if the element happens to get positioned near the edge so it overflow a little bit. Plus in general it better fits our "fail visible/open" policy in CSS - you should usually design defaults so that if the author doesn't think about it the content is still visible, even if it makes the page ugly.
Okay, so I'm inclined to just go ahead and edit in #8724 and this one. I'll review #8200 as well to see if I want to go ahead and pull it in, too.
Or hm, maybe we should act as 'clip' if you have a specified -fallback-bound element, and visible if you don't. So the initial value should be an 'auto'.
I found a real use case for the visible
value: https://floating-ui.com/docs/autoPlacement
We can achieve the same by:
position-fallback-bounding-box
at the center of the viewport position-fallback-overflow: visible
Would visible
also allow replicating this?
https://github.com/w3c/csswg-drafts/assets/9271155/0360aa7b-b4e8-4c37-a0c5-341cd353c3dc
@clshortfuse
This looks like a different thing, which is closer to Floating UI's shift that tries to keep the element in the bounding box without switching position; while this issue is about what to do when it's overflowing the bounding box.
And we don't have full support for shift
yet. I have a hacky demo, but it doesn't work with position: fixed
, so it probably doesn't work for popovers.
Thinking about this more as I try to write it up - rather than clipping to the overflow box, would it make more sense for the default to be "clip to the bounds of every scroller between you and the specified anchor"?
In particular, if there are two scrollers between your CB and your anchor, and the innermost has the anchor in the scrollport but the outer has the entire inner scroller out of the scrollport, you still want to clip, I assume. If we rely on just the overflow box, even if you specify an additional one with anchor-overflow-bounds, it won't be able to capture both boxes.
I feel like we may still want some consistency between here and #8724. If we use a box for overflow clipping, we would very likely also want to use the same box to trigger position fallback.
Then everything gets really complicated if we need to consider a chain of clipping boxes. Imagine if someone wants to use the intersection of the ancestor scrollers of the anchor to trigger position fallback...
I don't have a good idea towards this direction. Some thoughts are:
If we use a box for overflow clipping, we would very likely also want to use the same box to trigger position fallback.
Yeah, I definitely agree. I'm talking about an additional clip - in addition (and maybe by default?) clip to the scrollers between the anchor-scroll element and them as well. Multiple values for the clipping property (which we should move to an anchor-*
name rather than position-fallback-*
name, probably anchor-clip
).
Like, anchor-clip: none | [ fallback || scroll ]
? Meaning "clip to the position-fallback-bounds elements and/or the scroll containers of the scroll-anchor element". Initial value scroll
? Or maybe none
, hm.
Imagine if someone wants to use the intersection of the ancestor scrollers of the anchor to trigger position fallback...
I don't think fallback with the scrollers is necessary; the intermediate scrollers don't have any particular reason to be layout-relevant to the positioned element, they're just ways to hide the anchor and thus things that the positioned element probably wants to be hidden by as well.
anchor-clip: none | [ fallback || scroll ]
SGTM
@wangxianzhu
I'd like to get your input here. Do you think anchor-clip: scroll
is implementable?
In particular, for each anchor-positioned element, we need to duplicate a bunch of clip nodes from the ancestor scrollers of the anchor.
Discussed with @wangxianzhu offline. This should be implementable in Chromium
The solution doesn't involve duplicating any clip nodes, but we can simply make PaintPropertyTreeBuilder
switch to the scroller's clip state for the target element, so there's no performance issues either.
A slightly revised idea: anchor-clip: auto | none
, where
auto
is the default value, meaning it should be clipped by all ancestor clip boxes of the default anchor1none
means no clipping, same as beforeThere's a possibility that in the future, this gets extended into anchor-clip: auto | none | fallback | <anchor-element>
, meaning this element should be clipped by all ancestor clip boxes of the target element of the value, and the impl should be pretty similar. Or even extended into a multi-value syntax. But everything feels like YAGNI at least for now.
Also auto
feels much easier for people to use, although it's semantically the same as scroll
you suggested previously
1 Note that we no longer have a separate anchor-scroll
element due to #8675
I'm wondering if the default value should be auto
or none
. Maybe none
is better.
It's pretty common to have a scroller with overflow: auto
, which doesn't show a clear clip boundary to users when the content doesn't overflow. However, it should still apply clipping for anchor-clip: auto
, which can be quite confusing to users, and developers may actuall want the positioned element (e.g. a tooltip) to overflow such a scroller.
On the other hand, it can be less confusing when the default value is none
, so that developers can opt-in when they need clipping.
I would say that in my experiments I, sometimes, wanted to have the auto
, but mostly wanted the none
(the current behavior).
Also, I imagine clipping would also work for overflow: hidden
, and that could mean that clipping by default could lead to the content being not accessible if it would go beyond the overflow of such elements.
I've coded my tooltips to be a psuedo top-layer, so they break through their anchor's boundary. For example, this is anchored to the button, but overlays above the Nav Drawer and its scrollbar on the left:
I can simulate a clip by dropping the z-index:
An example of "don't render" because the anchor is off-screen is applicable to what I call "semi-sticky" toolbars, where it scrolls aways on scroll down, but stickied on scroll up. If the keyboard is focused on a toolbar button, the tooltip is shown. But when the toolbar button is no longer more than 50%, then it meets my custom threshold of being "off-screen" and I close the tooltip.
And mid-hide when scrolling down:
The consequence of not doing this is the issue we know where a tooltip can appear when it shouldn't:
I'm using intersection observer on the anchor as well as on the tooltip itself. Because the tooltip isn't useful without what it's giving a tip for, I want it on the anchor. But there could be an instance where the tooltip gets occluded (eg: forced position) and then I want it to hide instead of reposition when partially occluded.
I'm unsure what anchor-clip
means here. Is it when the anchor (eg: the button) is clipped? Or do we mean to clip the tooltip? Is my terminology wrong? Is "anchor" the button or tooltip here?
For example:
viewport
+- scroller
| +- anchor
+- popover
Then anchor-clip: auto
makes popover
additionally clipped by scroller
Yeah, it basically gives you back the clipping that you would have if your positioned element was an abspos next to the anchor. Being able to have your positioned element elsewhere in the page is a big part of what makes anchor positioning powerful.
Also, I imagine clipping would also work for overflow: hidden
Yes, overflow:hidden is a scroller so it would act identically to overflow:scroll.
And we don't have full support for shift yet. I have a hacky demo, but it doesn't work with position: fixed, so it probably doesn't work for popovers.
@xiaochengh Will css-anchor
consider implementing a shift effect in the future?
@yisibl Yes, but I don't have a clear idea how it should work. Anyway, it's better to file a separate issue for it.
The CSS Working Group just discussed [css-anchor-1] Need ability to say "don't render" when anchor is off-screen
, and agreed to the following:
RESOLVED: we add something to deal with clipping in this context
How do non-trivial clips like clip-path/border-radius behave? Implementation wise it seems like unless its simple rects (which can be collapsed) it'll be very difficult.
It also wasn't clear whether the desire is to have the anchored element be clipped by the (immediate or all?) scroller(s) or whether any ancestor clip should play a part, including things like overflow-clip-margin
Tab was just describing this proposal to me, and I think... I can't see any use case for wanting to anchor to an object that's been scrolled completely outside its scroll container.
What you want is to be able to use the intersection of that scroll container and the anchor element inside it as the anchor box, and to hide the anchorpos when the anchor box clips down to zero.
Okay so while it's good that we got a resolution to do something in this space, we still need to work out details.
As far as I can see, there are two independent cases we might want to address:
(2) might be most simply solved by allowing a positioned element to be clipped by its inset-modified containing block? The sidenote case, for example, would look like:
.sidenote {
position: fixed;
top: anchor(--container top);
bottom: anchor(--container bottom);
left: anchor(--container right);
right: 0;
anchor-default: --target;
align-self: anchor-center;
clip-to-containing-block: true;
}
This does rely on anchor-center
for positioning, tho; if you wanted any other alignment you'd be out of luck, since you're having to use your top/bottom to give yourself clipping bounds. It also relies on the scroll container having an anchor-name, rather than just relying on the nearest scroll container of the default anchor, which is probably what you want. So probably we do instead want to more explicitly reference the bounds of some scroller, defaulting to the default anchor element's nearest scroll container, like:
.sidenote {
position: fixed;
anchor-default: --target;
top: anchor(top);
left: anchor(--container right);
/* vertically clips to the bounds of the default anchor's nearest scroller */
position-clip: vertical;
/* inline-axis clips to the specified element, which must be a scroller */
position-clip: inline --some-container;
}
Hmm. More thought is needed here.
For the first case, we might be able to solve by simply saying that when your containing block is the scroller (or inside it) then you get clipped just like the contents of that scroller. See #9868. Then we can just have the second case be the behavior in all other cases.
Tab and I discussed the uses cases in this issue and #9380 and tried to come up with a proposal to handle them all. There are a few major cases to address:
position-container
. See earlier comment and issue 9868.position-try-options
result in overflow. See issue 9380.Our current suggestion for handling both 2 and 3 is a new position-visibility
property:
position-visibility: always | [ anchors-valid | anchors-visible ] || no-overflow
always
is the initial value; the abspos is always visible (as currently)anchors-valid
hides the abspos if any of the anchors it depends on are missing. Absposes depend on their default anchor element and any anchor element referenced in an anchor()
/anchor-size()
function without a fallback value.anchors-visible
hides the abspos if any of the anchors it references are missing or invisible (and it hasn't explicitly specified a fallback value), where invisible means either its not visibility: visible
or it's been clipped by overflow
of an intervening container. (We're also considering clip-path
for this check.)no-overflow
hides the abspos if it overflows its inset-modified containing block (after exhausting all position-try
options, if relevant). Note this value would apply even when using regular abspos or fixedpos.In all these cases, “hide” means that it (and all of its contents) are invisible (like ''visibility: hidden'') and do not contribute to scrollable overflow.
- where invisible means either its not
visibility: visible
or it's been clipped byoverflow
of an intervening container. (We're also consideringclip-path
for this check.)
Can you clarify what is "it" here, is it the anchor element's border box? Or does it include ink overflow? Or is it something else?
We're discussing a similar issue with view transitions in #8282 in trying to define what "offscreen" means for vt elements, so having a common definition for when we consider something not user-visible-enough would be great.
Currently the definition is up in the air. I agree we should be consistent with VT (and scroll animations?).
In all these cases, “hide” means that it (and all of its contents) are invisible (like ''visibility: hidden'') and do not contribute to scrollable overflow.
One thing that we will need to handle: make the position-visibility
transition in the same way as visibility
itself, from Web Animations spec:
For the visibility property, visible is interpolated as a discrete step where values of p between 0 and 1 map to visible and other values of p map to the closer endpoint; if neither value is visible then discrete animation is used.
A related note: if the position-visibility
will not work in the same as visibility
in a “can be overridden on a child” way, this new property could be used as a hack for “non-overridable” visibility (given the element is abspos). If this will be the case, we could want to think about introducing a new keyword to visibility
itself that will make it possible to achieve the same effect (making it non-overridable on the children). I wrote an article on visibility at some point, and I remember encountering use cases for this behavior (but that is probably better to open as a separate issue if there is no such yet).
All of that said, would it be possible to somehow make it apply the visibility
itself? It will make things a lot simpler from the author standpoint.
In all these cases, “hide” means that it (and all of its contents) are invisible (like ''visibility: hidden'') and do not contribute to scrollable overflow.
I think we should amend it to remove the part about not contributing to scrollable overflow. Here's why:
Making it no longer contribute to scrollable overflow would require layout to happen in response to scroll or try-block layout+style effects, for the no-overflow
and anchors-visible
keywords. This would either be circular or introduce the potential for significant complexity in the layout and style engine.
Anchored elements will need to avoid causing layout overflow for their containing blocks anyway, or else there would be useless scrollbars for that element even when the anchored element is not hidden.
I'm good with the position-visibility
proposal @fantasai wrote with this small change, and think it is easily implementable.
Making it no longer contribute to scrollable overflow would require layout to happen in response to scroll
Isn't that already the case because scrolling can change the position of the anchor such that a different fallback needs to be used for the anchored element? Since scrolling is threaded I figured the re-layout for such cases happens whenever the main thread receives the scroll offset, so possibly not perfectly in sync with scrolling.
Isn't that already the case because scrolling can change the position of the anchor such that a different fallback needs to be used for the anchored element? Since scrolling is threaded I figured the re-layout for such cases happens whenever the main thread receives the scroll offset, so possibly not perfectly in sync with scrolling.
The adjustment of the position of an anchored element due to scroll doesn't affect layout overflow (or shouldn't--maybe the anchor-positioning spec doesn't spell this out yet). This is similar to how sticky position offsets once the element becomes "stuck" also don't affect it.
The adjustment is currently specified as an additional translate, so without further specification it would have the same behavior as translations, which do affect overflow. But we can change that.
The CSS Working Group just discussed [css-anchor-1] Need ability to say "don't render" when anchor is off-screen
, and agreed to the following:
RESOLVED: Add position-visibility as proposed in the issue with concerns noted as issues in the draft, to the editors draft *after* publication of working draft.
@fantasai @tabatkins should I file a separate issue to clarify how we decide whether the anchor is offscreen? This comment suggested taking an intersection of the scroll container with the anchor element but its unclear what the reference box is (border box or ink overflow rect). And also why use the nearest scroll container instead of the viewport, what happens if the scroll container itself is offscreen. Would love to get a common definition of whether an element is onscreen since VT needs the same concept.
how we decide whether the anchor is offscreen?
I think we should use the same definition as IntersectionObserver viewport visibility. That is simple, consistent, makes it easy for implementations to use that to hide or show the anchored element, and at least one developer mentioned on this issue they already use this definition in their polyfill.
IntersectionObserver has a mode to check occlusion. I'm assuming we're not including that here?
IntersectionObserver has a mode to check occlusion. I'm assuming we're not including that here?
Correct, we would not use that mode. We'd just use the one that applies clips and filters, not the "v2" one.
We'd just use the one that applies clips and filters
Currently IntersectionObserver v1 doesn't respect filters, see https://github.com/w3c/IntersectionObserver/issues/509 and https://crbug.com/40937025. However, internal intersection observers respect filters to follow the corresponding specs (e.g. contents-visibility). We may need to either resolve this discrepancy in intersection observer spec or specify we need to consider filters here in css-anchor-1.
We'd just use the one that applies clips and filters
The algorithm [here](https://w3c.github.io/IntersectionObserver/v2/#calculate-intersection-rect-algo:~:text=Let%20intersectionRect%20be%20the%20result%20of%20running%20the%20getBoundingClientRect()%20algorithm%20on%20the%20target.) uses the result of getBoundingClientRect()
as the element's box to intersect against. IIUC that's based on border box so won't include overflow from filters like blur. If you want to include filters, wouldn't using the element's ink overflow rect be better?
The algorithm [here](https://w3c.github.io/IntersectionObserver/v2/#calculate-intersection-rect-algo:~:text=Let%20intersectionRect%20be%20the%20result%20of%20running%20the%20getBoundingClientRect()%20algorithm%20on%20the%20target.) uses the result of
getBoundingClientRect()
as the element's box to intersect against. IIUC that's based on border box so won't include overflow from filters like blur. If you want to include filters, wouldn't using the element's ink overflow rect be better?
I suggest we take this particular point to the Intersection Observer spec.
And also why use the nearest scroll container instead of the viewport, what happens if the scroll container itself is offscreen. Would love to get a common definition of whether an element is onscreen since VT needs the same concept.
What we want is that the element is clipped away "entirely", by some combination of overflow clip rects (and maybe other clipping operations?). This should include all the scroll containers up to the nearest inclusive ancestor of the positioned element's own containing block, at least; higher than that and you're already guaranteed that both elements are getting clipped by the same scroll container, so there's no need to specially care about them.
As for what precisely to consider "the box" that is getting clipped, I agree we should defer to what IO/etc are doing for consistency.
I suggest we take this particular point to the Intersection Observer spec.
I think we still need to address the filter issue here. See #issuecomment-1994974242.
Another discrepancy from the IntersectionObserver spec is about visibility
of the anchor element. IntersectionObserver doesn't consider visibility
. This is mainly a note for implementation not to merely rely on IntersectionObserver for posiiton-visibility: anchor-visible
.
One thing that we will need to handle: make the
position-visibility
transition in the same way asvisibility
itself, from Web Animations spec:
Do we need to handle the transition of actual visibility (not the value of position-visibility
property)? This seems only feasible if the spec defines a new visibility
value and lets the UA update the value of visibility
when the visibility requirement of the anchored element changes.
Or can we just use the existing values of visibility
(i.e. allow descendants to override with visibility: visible
)? The author will have the flexibility to just hide part of the anchored subtree. It may look weird, but it could be a choice of the author.
Edit: defining the visibility property in spec may limit the ability of implementation. I think it's reasonable to require non-overridable invisible status of the subtree for now. In the future, we could create new APIs to define the styles of visible/invisible states for fancier effects, e.g. fading in/out with opacity transition.
no-overflow
hides the abspos if it overflows its inset-modified containing block (after exhausting allposition-try
options, if relevant). Note this value would apply even when using regular abspos or fixedpos.
Can you clarify which of the following of the abspos should be used to check if it overflows its inset-modified containing block?
Edit: Now I understand this depends on the Overflow Management chapter. Chromium's implementation of position-try
checks overflow using the margin box of abspos.
During the implementation of anchors-valid
and anchors-visible
, I found the following things may need clarification about validity/visibility of anchors:
display: none
,content-visibility: hidden
, orcontent-visibility: auto
and is currently not rendered.Our current plan is to treat them as invalid because the anchor doesn't exist in the layout tree.
border-width: anchor(--a top)
an invalid anchor reference or should it be ignored?@position-try
styles?width: anchor-size(width)
without a default anchor) be treated as invalid or be ignored?anchor-size()
?Our current plan is to only check for validity and visibility for anchor references that are actually resolved, but not including those in unused @position-try
s.
Consider: an anchor is inside of a scroll container. The positioned element is outside of the scroll container. This is legit if you want the positioned element, when visible, to be able to project outside of the scrollable area, such as into the margin.
However, this then means that if the anchor is scrolled outside of the scrollport, the positioned element just hovers above or below the scroll container, wherever the anchor is theoretically positioned. That is almost certainly not what you want!
So we should have some way to specify that if the anchor is off-screen, the positioned element should hide itself.
Questions: