csssticky

Sticky sub-headers on a page with fixed page header


I have a web page with a fixed top-of-page header (website name, menu, etc.) and content that has sub-headers that I'd like to be sticky. I've thought of two ways of doing this in CSS, and neither works:

  1. Use position: fixed or position: sticky for the page header. This causes the rest of the page to scroll underneath the page header, and so its sub-headers aren't visible when they stick. They stick to the top of the page (which is underneath the page header), not to the top of their own visible section.

    When specifying the sticky sub-header, I could make top equal to the height of the page header. This fixes the stickiness at the expense of having to know the height (in my case it's variable).

    Another drawback of this approach is that the page scrollbar extends into the page header instead of being restricted to the scrolling section.

  2. Use flex or grid or whatever to break the page vertically into two sections, the header and a scrollable main section. This causes the sub-headers to properly stick to the top of the main section, but has two problems. One is that #fragments no longer work reliably, because the anchors are inside a nested scrolling block. The second, more serious problem, is that it triggers all sorts of bugs in mobile Safari where the browser tries to vertically center focused text input fields, and it does that by forcing the entire page to scroll (rather than just the main section). Once this happens the view is stuck and the page must be reloaded to get it to scroll properly again. Here's someone else's StackOverflow bug about this and their video of the problem. (That bug can happen without the nested scrolling div, but is much worse with it.)

Seems like a fixed page header is a very common thing, and wanting sticky sub-headers is a common thing, and I can't get both. I've investigated various high-profile mobile websites (Google Docs, reddit, StackOverflow, MDN), and they all use option #1 above and forego sticky sub-headers.

Any ideas for how to get sticky sub-headers to work without making my own nested scrolling block? Or for how to get around Safari's scrolling bug?


Solution

  • I ended up using solution #1, with position: sticky for the page header and a non-zero top property on the sticky sub-headers. Since the value of the top property should be the height of the page header, and that height is variable, I used JavaScript to set it:

    function configureStickyHeaders() {
        const pageHeader = document.querySelector(".pageHeader");
        if (pageHeader === null) {
            return;
        }
    
        const resizeObserver = new ResizeObserver(entries => {
            if (entries.length > 0 && entries[0].borderBoxSize.length > 0) {
                // Get the height of the page header.
                const height = entries[0].borderBoxSize[0].blockSize;
    
                // Set this variable, which is used by sub-headers.
                document.body.style.setProperty("--page-header-height", `${height}px`);
            }
        });
    
        resizeObserver.observe(pageHeader);
    }
    

    Here's the CSS:

    body {
        /* Updated in JS, see configureStickyHeaders(). */
        --page-header-height: 0;
    }
    
    .sub-headers {
        position: sticky;
        top: var(--page-header-height);
    }
    

    This approach still has the drawback of the scrollbar going to the very top of the page, but that's minor.