CSS - Position

The anatomy of visually-hidden

Visually-hidden styles are used to hide content from most users, while keeping it accessible to assistive technology users.

It works because the content is technically visible and displayed — it appears in the accessibility tree and the render tree, both of which are used by assistive technologies — it’s just that the rendered size is zero.

Our industry has largely settled on a standard CSS pattern for this, refined over years of testing and iteration, by many people. This pattern:

Code language: CSS

.visually-hidden {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

[…]

This article is not about when or why you would use visually-hidden content. There’s a number of excellent articles that discuss these questions in detail, notably Scott O’Hara’s Inclusively Hidden. But most of them don’t go into much detail about the specific CSS involved — why do we use this particular pattern, with these specific properties? So today I’m going to dissect it, looking at each of the properties in turn, why it’s there, and why it isn’t something else.

What forces layout/reflow: a comprehensive list by Paul Irish

All of the below properties or methods, when requested/called in JavaScript, will trigger the browser to synchronously calculate the style and layout*. This is also called reflow or layout thrashing, and is common performance bottleneck.

Generally, all APIs that synchronously provide layout metrics will trigger forced reflow / layout. Read on for additional cases and details.

An event for CSS position: sticky

Here’s a secret: You may not need scroll events in your next app. Using an IntersectionObserver, I show how you can fire a custom event when position:sticky elements become fixed or when they stop sticking. All without the use of scroll listeners.

[…]

One of the practical limitations of using CSS sticky position is that it doesn’t provide a platform signal to know when the property is active. In other words, there’s no event to know when an element becomes sticky or when it stops being sticky.

Fixed and sticky headers and in-page anchors

I made a little tweak to The Session today. The navigation bar across the top is “sticky” now—it doesn’t scroll with the rest of the content.

I made sure that the stickiness only kicks in if the screen is both wide and tall enough to warrant it. Vertical media queries are your friend!

But it’s not enough to just put some position: fixed CSS inside a media query. There are some knock-on effects that I needed to mitigate.

I use the space bar to paginate through long pages. It drives me nuts when sites with sticky headers don’t accommodate this. I made use of Tim Murtaugh’s sticky pagination fixer. It makes sure that page-jumping with the keyboard (using the space bar or page down) still works. I remember when I linked to this script two years ago, thinking “I bet this will come in handy one day.” Past me was right!

The other “gotcha!” with having a sticky header is making sure that in-page anchors still work. Nicolas Gallagher covers the options for this in a post called Jump links and viewport positioning. Here’s the CSS I ended up using:

Code language: CSS

:target:before {
    content: '';
    display: block;
    height: 3em;
    margin: -3em 0 0;
}

I also needed to check any of my existing JavaScript to see if I was using scrollTo anywhere, and adjust the calculations to account for the newly-sticky header.

Anyway, just a few things to consider if you’re going to make a navigational element “sticky”:

  1. Use min-height in your media query,
  2. Take care of keyboard-initiated page scrolling,
  3. Adjust the positioning of in-page links.