Open jonjohnjohnson opened 6 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 (&) */
}
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?
Would it be useful to collect more of them?
@johannesodland yes, definitely.
VS Code recently launched a feature called 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.
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.
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;
}
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.
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
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. )
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
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)
^ 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.
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?
Hmm, right, there's no circularity. So maybe it can work.
I think this should better be discussed in its own issue though.
Done, moved to a new issue — https://github.com/w3c/csswg-drafts/issues/8905 :)
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
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:
position-container
(#9868).
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...
After setting an element as
position: sticky
those bits are gathered in these ways...overflow
set correctly).inline
withposition: relative
and valid box offsets).auto
, such asbottom: 0px
.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
EXP Nested lists with sticky headers/labels
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.
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...
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 to0
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.