Movement and Visibility Detection with CSS and JS 👀
Tags: css, javascript, showdev, webdev
Published: 2022-10-24 12:03:05

Although CSS has added so many features recently, do we still have to use JavaScript to detect when an element becomes visible?

It depends:
- We don't need JavaScript for [lazy-loading images](https://stackoverflow.com/questions/65378621/lazy-loading-images-with-accessibility-and-printer-support) anymore.
- [Scroll-linked animations without JavaScript](https://css-tricks.com/practical-use-cases-for-scroll-linked-animations-in-css-with-scroll-timelines/) will be coming to CSS.
- But we still need [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to animate images when they first become visible.
- We also need IntersectionObserver to check the visual state of a sticky header element.

As this year's [State of CSS](https://stateofcss.com/)  survey is open for contributions, we can look back on a year of CSS innovation and better cross-browser support. [Extended color space](https://dev.to/ingosteinke/ccs4-colors-5bj2), [container queries](https://dev.to/ingosteinke/a-css-container-queries-example-1le0), and [parent/child selectors](https://dev.to/ingosteinke/css-hasparent-selectors-287c), top wish list items for years, are now available in modern web browsers.

But we still struggle with visibility detection. There is no `:visible` and no `:stuck` selector to select elements based on their visibility or sticky state.

## Not so simple Use Cases

An apparently simple use case could be a single page portfolio website. We have some pictures, some text, some links, and the contact information. A minimalist website focused on images and visuals, so we want to add some animation. We might also want to shrink or hide the header when scrolling down and make it appear when scrolling up again.

### Can we do this without JavaScript?

Both use cases are best solved in JavaScript using `IntersectionObserver`, which has a better performance than implementation alternatives based on `scroll` event handlers. Scroll detection can waste performance, time, and energy, blocking our main thread and makes websites feel slow and sluggish. `IntersectionObserver` is a modern API optimized by browser engines.

But `IntersectionObserver` is also an API that requires some boilerplate code which is not easy to understand and learn, especially as a beginner or as a web developer focused on CSS and web design.

I will use an animation example in this post, as there are already a lot of blog posts about sticky headers and their complicated details, especially when it comes to compatibility with Safari browsers on iPhones that don't get the latest updates anymore.

## Minimal Movement on Visibility Detection

As micro-animations are trending in web design, we want to animate elements when they come into view: when scrolling down a website, we could make a teaser text move in a subtle way, or make an image enter the viewport.

### Animate.css as Animation Library

Simple animations, technically defined as transitions with animation keyframes, don't need to be reinvented. [Animate.css (animate.style)](https://animate.style/) is a popular open source CSS animation library that offers a set of predefined animations which can be customized and configured.

Animating a heading or an image becomes quite easy with `animate.css`, as we only need to add two class names: `animate__animated`, and another `animate__*` class to choose an animation effect.

### Adding Visibility Detection

But without adding a visibility detection, the animations don't wait for the user to view the element, so they might already have finished when the user scrolls the elements into view. A simple visibility detection using `IntersectionObserver` is not that simple after all. We need to  

- initialize an observer object, 
- tell it what elements to observe, 
- make sure we use the correct container element
- adjust the threshold for the visibility trigger
- code a callback function that
  - iterates over all triggered element,
  - tests the visibility criteria again (why? see below 👇),
  - and adds the animation classes to start the actual animation.

### Code Example and our animated Website

You can check the commented code pen and a real-world use case below.

Notable details:
- The code example is written in classic JavaScript (< ES6) so that we don't need Babel transpilation for older browser support.
- While we might expect the observer only triggering when an intersection happened at the threshold configured in the observer's options object, we still need to ensure every `intersectingEntry` `isIntersecting` and its `intersectionRatio`is greater than our `observerOptions.threshold`.

It seems counter-intuitive that we have to check the intersection criteria in our callback. I would have expected the IntersectionObserver to return only elements that have changed in the entries array. But it seems that initializing an IntersectionObserver causes an intial function call with an array that contains all observable objects, whether they are intersecting or not. See this [StackOverflow answer from 2017](https://stackoverflow.com/a/47855484/5069530) for more details.

Allowing for non-intersecting entries also adds the **opportunity to handle elements that are not visible anymore**.

- We don't initialize the observer until all DOM elements are present (loaded), so that we don't miss any element that we want to animate:
`document.addEventListener('DOMContentLoaded'`...
- Elements' `data-animationclass` attributes hold the class name for an animation, to be set as an actual class name using `classList.add`, by our intersection handler callback function, so that each element can have a different class name.

{% codepen https://codepen.io/openmindculture/pen/jOxKpoe %}

For the actual portfolio website, I added another attribute, `data-animationclassincolumn` to be used instead of `data-animationclass` when the animated element is displayed within a column (portrait) layout like on a small smartphone screen.  So we can use different animation directions to adapt our animations to alternative screen layouts.

Although our websites might often be complex enough to use custom logic and some JavaScript anyway, there are simple situations where we could do without the boilerplate code and just use some CSS to style a website. So this is my conclusion:

## Future CSS Visibility Innovation?

Let's keep our eyes open for upcoming CSS innovation. I hope that there will be something like a `:visible` selector so we can handle simple use cases like this, without redundant boilerplate JavaScript code. Then it will become easier to focus on visual web design in the future.