Hopp til innhold

Intersection observer

Leiv Fredrik Berge

2020.08.21

Use the intersection observer api to unlock cool UX features in your web application. Fremtind provides a thin wrapper hook in the @fremtind/jkl-react-hooks package, to make it easier to use intersection observers in React.

The obvious

The obvious use case is to lazy load content. When the content is outside the viewport, it doesn't provide your users with anything (at least untill they scroll). So it would be pretty nice to just defer loading the content that is below the fold. In this example we'll use the observer to set opacity on the element, with a transition and little delay you'll see the element fade into the viewport when you scroll it in.

Lets look a bit closer at how useIntersectionObsever work.

import React from "react";
import { useIntersectionObserver } from "@fremtind/jkl-react-hooks";

const MyComponent = () => {
    useIntersectionObserver(targetRef, onIntersect, fallback, options);

    return null;
};

Load the hook into you React component and follow the rules of hooks. The intersection hook takes a reference to a component, a function that is fired by the observer, a fallback function and a options object. Lets add the reference.

import React, { useRef } from "react";
import { useIntersectionObserver } from "@fremtind/jkl-react-hooks";

const MyComponent = () => {
    const targetRef = useRef(null);
    useIntersectionObserver(targetRef, onIntersect, fallback, options);

    return <div ref={targetRef}>content</div>;
};

Like that, we've loaded the useRef hook from React, and applied that reference to our returning div. If you want to do this with your custom components, forwardRefs might be useful.

import React, { useRef } from "react";
import { useIntersectionObserver } from "@fremtind/jkl-react-hooks";

const MyComponent = () => {
    const targetRef = useRef(null);
    const [isIntersecting, setIsIntersecting] = useState(false);
    const onIntersect = (entires) => {
        setIsIntersecting(entires.some((entry) => entry.isIntersecting));
    };
    useIntersectionObserver(targetRef, onIntersect, fallback, options);

    return <div ref={targetRef}>content</div>;
};

The buisness end of the hook is the onIntersect callback. This is a function where you get an array of observered elements back. In this case we want to see if anything in the array is intersecting with the viewport. We need to store that somewhere, so we'll add a useState hook to hold the intersection value.

import React, { useRef } from "react";
import { useIntersectionObserver } from "@fremtind/jkl-react-hooks";

const MyComponent = () => {
    const targetRef = useRef(null);
    const [isIntersecting, setIsIntersecting] = useState(false);
    const onIntersect = (entires) => {
        setIsIntersecting(entires.some((entry) => entry.isIntersecting));
    };
    const fallback = () => setIsIntersecting(true);
    useIntersectionObserver(targetRef, onIntersect, fallback, options);

    return <div ref={targetRef}>content</div>;
};

Browser support for the intersection observer api is pretty strong, but lets get it out of the way, IE 11 do not support this API. So if you need to support IE11 in your application, use the fallback function to set the component in the state you need. Here we need to make sure the content is visible for IE users, without this, they will never load the content. Enough about IE11, I don't want to make myself more aggrevated.

import React, { useRef } from "react";
import { useIntersectionObserver } from "@fremtind/jkl-react-hooks";

const MyComponent = () => {
    const targetRef = useRef(null);
    const [isIntersecting, setIsIntersecting] = useState(false);
    const onIntersect = (entires) => {
        setIsIntersecting(entires.some((entry) => entry.isIntersecting));
    };
    const fallback = () => setIsIntersecting(true);
    const options = { rootMargin: "0px", threshold: [0.0, 1.0] };
    useIntersectionObserver(targetRef, onIntersect, fallback, options);

    return <div ref={targetRef} style={{ display: isIntersecting ? "block" : "none" }}></div>;
};

Finally we need to supply an options object. This provides the constraints for the observer, when it should fire and where its margins should be.

Now this is the most basic example, but it's still useful and quite easy to expand. Lets do that, expand.

The not so obvious

See it live here. Codesandbox flakes out abit when its inside a iframe, so maybe don't try to do fancy stuff with intersection observers inside iframes. Anyways, these couple of files creates a pretty cool little effect, where the header changes style and attaches to the top of the viewport, using the intersection observer and position sticky.

The magic here is to play with the rootMargins. By setting it to -98% of the bottom, the intersecting happens at the top of the container, not the bottom as usual. Then we can use that to change the class name and the apperance of the div. This shows that you can use the observer for more than just lazy loading content below the fold. This can enable us to more closely replicate native app feeling in our webapps.

// IntersectionComponent.jsx
import React, { useRef, useState } from "react";
import { useIntersectionObserver } from "@fremtind/jkl-react-hooks";

export const IntersectionComponent = ({ children }) => {
    const [isIntersecting, setIsIntersecting] = useState(false);
    const targetRef = useRef(null);
    const onIntersect = (entries) => setIsIntersecting(entries.some((entry) => entry.isIntersecting));
    const fallback = () => console.log("useful for browser that do not support intersection observer");
    const options = {
        rootMargin: "0px 0px -98% 0px",
        threshold: [0, 1.0],
    };
    useIntersectionObserver(targetRef, onIntersect, fallback, options);

    return (
        <div className={`target ${isIntersecting ? "target--small" : "target--large"}`} ref={targetRef}>
            <div>{children}</div>
        </div>
    );
};
@use "~@fremtind/jkl-core/jkl";

.target {
    overflow: hidden;
    transition: jkl.easing("exit") jkl.timing("lazy");
    padding: 1rem 0.5rem;
    transition-property: color, font-size, box-shadow, background-color;
    & > div {
        transition: jkl.easing("exit") jkl.timing("lazy");
        transition-property: transform;
    }

    &--small {
        color: rebeccapurple;
        box-shadow: 0px 5px 4px -6px rgba(0, 0, 0, 0.75);
        font-size: 2rem;
        background-color: white;
        position: sticky;
        top: 0;
        & > div {
            transform: translateX(0%);
        }
    }
    &--large {
        color: black;
        font-size: 3rem;
        background-color: transparent;
        & > div {
            transform: translateX(10%);
        }
    }
}

Focus on mobile

On desktop, focus and hover states help interactive elements stand out. We don't have the same luxury on mobile, but we can use the intersection observer to know when an element is in a prime clickarea, and set the hover state programmaticly. See code, open preview in separate tab to actually see the effect. Mark that this is not hover, the mouse in complety stationary, the effect is triggered by the intersection.

References