w3c / csswg-drafts

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

[css-position] ‘Sticky’ behavior is too limited as position scheme created from the elements nearest block level ancestor & margins #2496

Open jonjohnjohnson opened 6 years ago

jonjohnjohnson commented 6 years ago

https://drafts.csswg.org/css-position-3/#sticky-pos

TLDR Should "sticky" be defined by its own property that sets edge and distances within a containing block defined by the elements "offset parent".

I know that "sticky" behavior has already landed in all major browsers, buuuut I'm still gonna lay out how it confuses me after putting it through the ringer.

Rehashing Sticky

When something is "sticky" it boils down to these bits...

  1. Scrolling context
  2. Box to stick within inside scrolling context
  3. Sticky edge
  4. Distance from sticky edge
  5. Distance from box's opposite edge to end sticking.

After setting an element as position: sticky those bits are gathered in these ways...

  1. Nearest ancestor scrolling in the axis of of sticky edge (bar any ancestors between the element and scroll context having overflowset correctly).
  2. Elements containing block being its nearest block level ancestor (often defined by parent element, though ancestors between could still be inline with position: relative and valid box offsets).
  3. Any box offset not computed as auto, such as bottom: 0px.
  4. The value of the that set box offset.
  5. Computed from both the margin of the element on the opposite edge to which is sticking AND the opposite edge of its containing block content box.

Issue 1 - Dual implication of the margin property?

The easiest issue to spot with this is mixing the use of margin for both normal flow AND the "sticky constraint rectangle". Its use for "SCR" in any meaningful way forces an author to offset that margin in normal flow with a sibling element, getting nice and hacky.

EXP Bottom sticky flag that's hidden in the fold, but scrolls in just after

.flag { position: sticky; bottom: 0px; margin-top: var(--height-scrollport); }
.before-flag { margin-bottom: calc(-1 * var(--height-scrollport)); }

EXP Nested lists with sticky headers/labels

.label { position: sticky; top: 0px; margin-bottom: var(--height-label); }
.after-label  { margin-top: calc(-1 * var(--height-label)) }
.label-nested { position: sticky; top: var(--height-label); }

So why not gather this length in a different way? Similarly to why scroll-margin was created to not just infer or force *hacky layouts for an author to fully leverage snapping?

Issue 2 - Containing block as nearest block level ancestor?

Some aspects of layout require particular structures (like what's shown in this spec), so using the elements nearest block level ancestor instead of allowing authors more control over the creation of a "sticky constraint rectangle", has major tradeoffs.

It's actually hard to get this behavior otherwise as if you wrap a box around the sticky element for an anchor it will contain it and prevent the sticky element from moving.(https://github.com/w3c/csswg-drafts/issues/1459)

I'd imagine using the box of the elements nearest positioned parent would give more control to an author instead of current proposal? Or at least using the "offset parent" until reaching the scrollport, then using that scrollbox?

Issue 3 - Other position schemes would be useful?

When we are already limited by the current containing block definition for "SCR", it's worse that we can't even use another position scheme to move the element around within that containing block. Not allowing for these position schemes, can force an author to sacrifice document semantics, when trying to place an element in a desired normal flow to then "stick" based upon that shifted position. Think of simply sliding an element around "relatively" or even "absolutely" pushing an element to the bottom side of its offset parent to then leverage "sticky" behavior at the bottom scrollport edge, when semantically the element isn't at the end of its sequence). And creating a hacky flex/order/margins/transform solutions doesn't fix the issues that come with not having access to relative/absolute position schemes. Even when losing flow placeholding with absolute, it can still be a desirable case.

Again, using an elements nearest positioned ancestor for the containing block could give meaningful positioning to the sticky element. Allowing the sticky element to leverage relative or absolute schemes for itself against its offset parent? I'd say even forcing that sticky behavior can only be applied to a positioned element in the first place, like how box offsets require position. Caveat being that fixed position affords no sticky affect.

Solution for all 3 issues?

In my understanding the information needed to set up sticky behavior (as well as proper optimizations, so one doesn’t need to set isolation or will-change, like is often needed now) is better suited from a separate property (and not creating a position scheme) such as...

sticky: bottom 40px 100vh;
sticky: top 0px 50px, left 0px 50px;

A property where you could set a list of "sticky" edges, with their corresponding sticky start distance from scrollport edge AND end distance in the opposite direction from the edge of its containing block. If just the edge is set, then lengths are computed as 0? If only the start is set, the end computes to 0 which is surely a common case. For the property to have any affect the element, it must be positioned with a meaningful relationship to its offset parent containing block to create a "SCR". I've scraped through the spec and created an assortment of experiments leveraging sticky-ness and it seems like what I'm proposing (though most likely far too late to ever be considered anything more than vain) would be viable.

bramus commented 2 years ago

Recently stumbled upon the limitation where I wanted multiple items to be sticky inside the same parent container, but without overlapping each other.

Given this markup:

<main>
<article>
  <h1 />
  <h2 />
  <h2 />
</article>
<article>
  <h1 />
  <h2 />
  <h2 />
</article>
</main>

I want the <h1> elements to be sticky against the top edge of <article>, and want the subheadings <h2> to stick against the preceding <h1> that's already stuck.

Right now I fixed it by fixating the top for <h2> to equal the height of <h1>, but that only works if all <h1> elements have the same height.

article :is(h1, h2) {
  position: sticky;
  top: 0;
}

article h2 {
  top: var(--height-of-h1); /* But what if the <h1> text wraps?! Uhoh! */
}

👉 What I'm looking for is an easier way to say “Hey <h2>, stick against the already sticky <h1>

Note that it's not a simple as saying “stick against the previously stuck element“, as you'd then end up with the 2nd <h2> sticking against the first <h2>, sticking against the <h1>.


💡 Could potentially be solved by leveraging some other specs and by expanding the functionality of selector() to mimic document.querySelector (see https://github.com/w3c/csswg-drafts/issues/5884#issuecomment-867117769)

h2 {
  top: selector("h1:has(+ &)"); /* Selects the h1 that precedes self (&) */
}
johannesodland commented 2 years ago

Came across an excellent peekaboo header example from @jakearchibald recently.

It uses an extra sibling element and margins to reposition the header element, while using both sticky top and bottom.

The extra sibling element is needed as other positioning schemes are not available when using sticky positioning (issue 3).

This could have been solved by the proposed solution:

position: relative;
top: 0px; /* dynamically updated to current offsetTop */
sticky: top calc(-1 * var(--height)), bottom calc(100% - var(--height));

I think there are plenty of "tricks" like this in the wild. Would it be useful to collect more of them?

bramus commented 2 years ago

Would it be useful to collect more of them?

@johannesodland yes, definitely.

bramus commented 2 years ago

VS Code recently launched a feature called sticky scroll:

sticky-scroll

It’s a great use case for an extension to position: sticky.


Linking back to my suggestion in #7475 which would turn position into kind of a shorthand, perhaps this type of syntax would also make sense here as well? Something like: position: sticky / depth.

By default the depth would be a number and default to 1. Elements with the same depth don’t stick against each other. Elements with “more” depth stick against elements that have a lower depth. Elements with a lower depth push elements with a higher depth value out of view.

Taking my markup from above as an example, the CSS to achieve it would then look like this (being overly verbose here, to indicate what is going on):

article h1 {
  position: sticky / 1;
}

article h2 {
  position: sticky / 2;
}

Alternatively, this depth could also be a separate property named sticky-level or sticky-depth or …

article :is(h1, h2) {
  position: sticky;
  top: 0;
}

article h2 {
  sticky-level: 2;
}

The inset and margin properties would remain untouched, and work like they already do wrt sticky positioning.

brechtDR commented 2 years ago

This is a great one. Have come across a few situations where this could come in handy. Mostly had to rely on a bit of JS for this in the past. This should be made possible in CSS. I do like the syntax with the slashes as it removes the need for an extra property. As long as we could handle this with variables, it seems ok to me. In complex matters we might want to store this. Just as it can be handy to store z-indexes in variables.

sillvva commented 2 years ago

It is technically already possible to do this with just pure CSS, but not as cleanly as the above examples. The only requirement is that each nesting level must be inside their own container. And doesn't work if the headers wrap, unfortunately.

I think this proposal would be a useful add-on for CSS.

https://codepen.io/Sillvva/pen/LYdmOda

<section>
  <h1>Header 1</h1>
  <section>
    <h2>Header 2</h2>
    <p></p>
    <p></p>
    <p></p>
    <section>
      <h3>Header 3</h2>
      <p></p>
      <p></p>
    </section>
    <section>
      <h3>Header 4</h2>
      <p></p>
      <p></p>
    </section>
  </section>
</section>
h1 {
  font-size: 2rem;
  padding: 0.25rem 0.5rem;
  position: sticky;
  top: 0;
  z-index: 3;
}

h2 {
  font-size: 1.6rem;
  padding: 0.25rem 0.5rem;
  position: sticky;
  top: 2.5rem; // h1 = 2rem font size + 0.5rem padding
  z-index: 2;
}

h3 {
  font-size: 1.2rem;
  padding: 0.25rem 0.5rem;
  position: sticky;
  top: 4.6rem; // h1 = 2rem + 0.5rem, h2 = 1.6rem + 0.5rem
  z-index: 1;
}
brechtDR commented 2 years ago

I wouldn't say that this example is completely the same. As you stated yourself, it requires a bit of messy semantics with "unneeded" wrappers. Also from the example of your codepen. When moving out of the content area, the headers get removed one by one starting with the deeper nested one first. Would be nice to have the sticky "scroll out / become unsticky' together.

WickyNilliams commented 2 years ago

Here's a proof of concept I made recently for stacking sticky elements with arbitrary nesting and no JS (JS is used for components but the effect does not require it).

It's complicated by not being able to do this kind of arithmetic directly in css:

--x: calc(var(x) + 1)

But otherwise works well: https://codepen.io/WickyNilliams/pen/MWVaPKz

My motivation for this was wanting the sticky parts of the design system I work on to stack, without making any assumptions about how components are combined, or any limitation on depth.

The big downside if having to hard code element heights into css. As pointed out further up the thread, this falls apart if lines wrap. So a property that lets the browser handles this would be ideal

johannesodland commented 2 years ago

Here's an other example of where sticky is useful with other positioning schemes (issue 3).

It's useful to use sticky positioning with abspos when you need to position elements in the background without taking up any place in flow. This is used in articles like these: https://www.nrk.no/hvis-insektene-forsvinner-1.15029017.

This would easily be solved with the proposed sticky property:

position: absolute;
top: 0px;
height: 100vh;
sticky: top 0px 0px;

These positioning schemes will be even more useful once we get scroll animations.

It's currently not possible to use abspos with sticky, but it's possible to emulate it using a combination of column flexbox, order: 0 and negative top margins.

I've tried to generalise the approach in this codepen. (It's quite ugly at the moment, I'll try to clean it up later. )

johannesodland commented 1 year ago

When the nearest scroll port is the viewport, it would be nice to be able to specify if the element should be sticky relative to the edges of the small, the large or the dynamic viewport. 🤔

Edit: filed https://github.com/w3c/csswg-drafts/issues/8934

kizu commented 1 year ago

Wanted at first to create a new issue, but thought that I could just comment in this one.

But then, moved it to its own issue in the end — https://github.com/w3c/csswg-drafts/issues/8905

(all text from this comment moved to the new issue)

xiaochengh commented 1 year ago

^ This is unlikely going to happen without dramatically changing how anchor() works.

anchor() requires the anchor element's layout to have no dependency on the anchored element, so that's why it only works on out-of-flows and has a bunch of criteria for acceptable anchors. Sticky positioned elements are in flow and will affect anchor element's layout.

kizu commented 1 year ago

While the sticky elements by themselves can affect the anchor element's layout, I don't think changing the inset properties of a sticky element would affect anything, as it is virtually just an offset change? So there would not be any circularity? Or am I missing some cases?

xiaochengh commented 1 year ago

Hmm, right, there's no circularity. So maybe it can work.

I think this should better be discussed in its own issue though.

kizu commented 1 year ago

Done, moved to a new issue — https://github.com/w3c/csswg-drafts/issues/8905 :)

johannesodland commented 1 year ago

Is seems like the CSSWG just resolved to ignore margins in sticky-pod calculations, possibly resolving issue 1 if it’s web compatible:

https://github.com/w3c/csswg-drafts/issues/9052#issuecomment-1642600755

johannesodland commented 2 months ago

The WG has resolved to specify a more generic position-container that changes the containing block in https://github.com/w3c/csswg-drafts/issues/9868. If the new property is generic and applies to sticky positioning, I recon this could solve issue 2.

To summarize the resent developments: