Add guide and evals for context-sensitive sticky headers#603
Add guide and evals for context-sensitive sticky headers#603patrickkettner wants to merge 6 commits into
Conversation
a52b78f to
3112e18
Compare
3112e18 to
388701f
Compare
|
Results of local Pass Rate - Unguided: 0%, Guided: 0%{
"summary": {
"unguidedMedian": 0,
"guidedMedian": 0,
"unguidedPassRate": 0,
"guidedPassRate": 0,
"unguidedPassed": 0,
"unguidedTotal": 0,
"guidedPassed": 0,
"guidedTotal": 0,
"runsPerTest": 1,
"expectedTotalRuns": 1,
"taskCount": 1,
"runCountPerTask": 1,
"guideUsageRate": 0,
"guideUsageCount": 0,
"totalGuidedRuns": 1,
"totalGuidedNonDisciplineRuns": 1,
"toolActivationRate": 0,
"toolActivationCount": 0,
"unguidedEarlyFailures": 0,
"unguidedEarlyFailureRate": 0,
"guidedEarlyFailures": 0,
"guidedEarlyFailureRate": 0,
"guidedNonDisciplineEarlyFailures": 0
},
"results": {
"task - context-sensitive-sticky-headers - guided": [
{
"runNumber": 1,
"results": [],
"guidesUsed": [],
"retrievedGuides": [],
"fileReadGuides": [],
"guidanceToolsUsed": [],
"discipline": "user-experience",
"isSkill": false,
"expectedToolPrefixes": [
"modern-web"
],
"guideName": "context-sensitive-sticky-headers",
"taskName": "task",
"baseApp": "daily-grind",
"prompt": "Add new section headers inside the main content area of the page (e.g., within the cards or sections). These headers should stick to the top when scrolling."
}
],
"task - context-sensitive-sticky-headers - unguided": [
{
"runNumber": 1,
"results": [],
"guidesUsed": [],
"retrievedGuides": [],
"fileReadGuides": [],
"guidanceToolsUsed": [],
"discipline": "user-experience",
"isSkill": false,
"expectedToolPrefixes": [
"modern-web"
],
"guideName": "context-sensitive-sticky-headers",
"taskName": "task",
"baseApp": "daily-grind",
"prompt": "Add new section headers inside the main content area of the page (e.g., within the cards or sections). These headers should stick to the top when scrolling."
}
]
},
"stats": {
"task - context-sensitive-sticky-headers - unguided": {
"medianPassRate": 0,
"runPassRates": [
0
],
"runCount": 1,
"isSkill": false,
"passedChecks": 0,
"totalChecks": 0,
"earlyFailures": 0
},
"task - context-sensitive-sticky-headers - guided": {
"medianPassRate": 0,
"runPassRates": [
0
],
"runsUsingGuide": 0,
"runsWithToolActivation": 0,
"runCount": 1,
"isSkill": false,
"passedChecks": 0,
"totalChecks": 0,
"earlyFailures": 0
}
},
"timestamp": "2026-04-22T19:40:06.347Z",
"runCount": 1,
"agent": "gemini_cli",
"serving": "skills_cli",
"model": "gemini-pro-latest"
}Given the 0% guided pass rate, this use case would require some additional investigation and fine tuning. Let's remove the eval files from this PR to unblock getting the guidance in and eng will follow up on the evals separately. |
|
@LeaVerou are you able to take a look at this one? |
Just saw this, will take a look in a bit! |
SME ReviewP0: Flashing when slightly scrolledThe guide downplays the problem quite a bit:
This is what the demo page looks like if the container is only slightly scrolled: Screen.Recording.2026-05-01.at.20.46.43.movIt gets way worse if you also reduce the font-size (as is often needed). Sometimes the CSS will fight against the scrolling even: Screen.Recording.2026-05-04.at.09.24.05.movAnd even worse if you move the transition inside the CQ (so that it only applies when you go from unstuck → stuck but NOT when you go stuck → unstuck): Screen.Recording.2026-05-04.at.09.56.16.movYou can trigger the issue reliably by adding this snippet to the demo and playing with <script>
let firstHeader = document.querySelector(".section > .sticky-container");
firstHeader.scrollIntoView({behavior: "smooth"});
setTimeout(() => {
// Adjust this number for different versions of the problem. Any number between 1-43 seems to trigger it.
const offsetY = 10;
document.scrollingElement.scrollTop += offsetY;
}, 1000);
</script>Avoiding "large enough" changes does not fix this, it just reduces the interval where this is observable. I could not find any box-model affecting change where the problem was not present at all. Even going from I did end up getting badly nerd sniped by this and doing original research to come up with workarounds 😅, which I'll publish in a separate blog post (I can drop a link here when it's out if it's of interest), but so far all workarounds I found have their flaws. The best one so far is adding an Screen.Recording.2026-05-04.at.09.30.14.movFor this particular case, a Screen.Recording.2026-05-04.at.09.34.14.movBut a In both cases you'd want But we probably want to be recommending tried and tested techniques for this, not brand new workarounds… So, I'm not quite sure what's the best way forwards. 😞 P2: DO always specify container nameGiven that these CQs may be nested, we should emphasize that these queries should always include a container name. P2: Not sure what "context-sensitive" refers toI don't see anything context sensitive about this? The description doesn't help either:
Fallback strategiesP0: No fallback in the demo pageThe fallback is not present in the demo page. It should not only be there, but it should be verified that it's there via the expectations! P1: Discuss PE as the default and frame the
|
|
Just saw the review request — GitHub didn't bother notifying me for some reason. I'll try to take a look soon.
I held off on it because after talking to @tabatkins he thinks it's either a browser bug or a spec bug, so I filed w3c/csswg-drafts#13898 for now so we can figure that out first. |
| ### Notes on Dimension Changes | ||
|
|
||
| **MANDATORY:** **DO NOT change box-model properties (height, padding, border, font-size) when an element becomes stuck.** | ||
| Because the browser performs a two-pass rendering update to resolve scroll-state queries, modifying any property that affects layout (even changing `font-size` from `100%` to `99%`) can cause the element to immediately become unstuck. This results in an infinite layout loop and severe visual flickering when users scroll slowly near the intersection point (specifically, within a 1-43px offset window). |
There was a problem hiding this comment.
The 1-43px offset window was for this specific case! I'm sure it varies per layout depending on the exact shrinkage.
(Also having to recommend against changing box-model properties is quite unfortunate, but also probably the right thing to do… :/)
There was a problem hiding this comment.
good news :D with overflow-anchor: none on the scroll container, the box-model restriction goes away.
| **MANDATORY:** **DO NOT change box-model properties (height, padding, border, font-size) when an element becomes stuck.** | ||
| Because the browser performs a two-pass rendering update to resolve scroll-state queries, modifying any property that affects layout (even changing `font-size` from `100%` to `99%`) can cause the element to immediately become unstuck. This results in an infinite layout loop and severe visual flickering when users scroll slowly near the intersection point (specifically, within a 1-43px offset window). | ||
|
|
||
| Adding a `transition` does not fix this flashing; in fact, putting the `transition` inside the `@container` query makes it even worse. Only change non-layout affecting properties such as `background-color`, `color`, `opacity`, and `box-shadow`. |
There was a problem hiding this comment.
transform (and individual transform properties) too! E.g. see https://codepen.io/leaverou/pen/xbROqRo for an example on how it can be used to fake shrinking.
There was a problem hiding this comment.
(addressed by the rewrite)
with overflow-anchor: none in place, transforms work fine alongside box-model changes. The new section explicitly calls out transforms as one of the things you can now safely animate in the stuck state.
Looks like |
Yes!! I was unsure about it being applied to It still worries me that it's disabling a supposedly positive behavior, and the lack of Safari support, but overall this does look very promising! The issue is also slotted to be discussed tomorrow, so I might have more data by then. |
|
awesome! Ill hold off on updating anything due to it until after I hear back |
Bottom line was: we need to fix this problem in CSS, unsure how we'll do it. I think recommending Also, another point that @tabatkins brought up in the call was that if an element is bottom stuck, |
|
@LeaVerou are you still waiting on changes? |
Co-authored-by: Lea Verou <lea@verou.me>
| } | ||
| ``` | ||
|
|
||
| Apply it to whichever element is the scroll container (typically `:root` for the document scroller). With this in place, you can freely change any property in the stuck state, including box-model properties and transforms. |
There was a problem hiding this comment.
You can apply on the parent of the position: sticky element and it seems to work equally well. Did that work less well in your testing? If not, I think the narrower, the better, we don't need to disable scroll anchoring everywhere in the document (it's usually a good thing!) because one table somewhere has a sticky header.
| Apply it to whichever element is the scroll container (typically `:root` for the document scroller). With this in place, you can freely change any property in the stuck state, including box-model properties and transforms. | ||
|
|
||
| ### Fallback strategies | ||
| {{ BASELINE_STATUS("container-scroll-state-queries") }} |
There was a problem hiding this comment.
How does BASELINE_STATUS work when different bcd keys have different support? Is it worth specifying a bcd key?
| {{ BASELINE_STATUS("container-scroll-state-queries") }} | ||
|
|
||
| Scroll state queries are a progressive enhancement. In browsers that do not support `container-type: scroll-state`, the header will still stick to the top (due to `position: sticky`), but it will not visually transform. For most use cases, this is the recommended approach. | ||
|
|
There was a problem hiding this comment.
I was expecting something like this to come after:
| If stickiness is not essential, but `position: sticky` without different styling would be a worse experience than no stickiness at all, you can wrap the `position: sticky` and related declaration in an `@supports` query as well: | |
| ```css | |
| .sticky-container { | |
| @supports (container-type: scroll-state) { | |
| position: sticky; | |
| top: 0; | |
| container-type: scroll-state; | |
| container-name: section-header; | |
| z-index: 10; | |
| } | |
| } | |
| ``` |
|
|
||
| Scroll state queries are a progressive enhancement. In browsers that do not support `container-type: scroll-state`, the header will still stick to the top (due to `position: sticky`), but it will not visually transform. For most use cases, this is the recommended approach. | ||
|
|
||
| **Tip:** If your "stuck" styling requires a different background color for readability, consider setting your layout up so that the default styling works everywhere (Progressive Enhancement). If you must use a fallback, you can gate the `position: sticky` behaviour itself inside an `@supports (container-type: scroll-state)` query. |
There was a problem hiding this comment.
Ah, just noticed it's mentioned here, I missed it in my first skim, apologies. Leaving the suggestion above in case you prefer that wording, but feel free to resolve if not!
LeaVerou
left a comment
There was a problem hiding this comment.
Only thing I'd consider blocking is the point about overflow-anchor — I'd really rather avoid applying it to root unless we really need to. In my testing it seems fine applied to the sticky parent: https://codepen.io/leaverou/pen/JobKgJQ
@patrickkettner curious if you came across cases that need it on the root?
Fixes #256