0.2.0 • Published 2 months ago

@wasamistake/use-intersection-animation v0.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

useIntersectionAnimation

https://github.com/wasamistake/use-intersection-animation/assets/150041252/c11edb0b-6126-40c7-917b-368a9f89d017

This is a React hook to animate/reveal elements when they enter the viewport (or another root element). It uses the Intersection Observer and the Web Animations APIs to achieve that, which results in a package that is small and performant.

Some built-in effects are also packaged for convenience.

Usage

First, install the package with npm install --save-exact @wasamistake/use-intersection-animation. As there will probably be breaking changes before version 1, --save-exact will minimize the incidence of problems related to that.

Once the package has been installed, import useIntersectionAnimation from @wasamistake/use-intersection-animation.

A common use case will look something like this:

import useIntersectionAnimation, {
  type SyntheticEffect,
} from '@wasamistake/use-intersection-animation'

const reveal: SyntheticEffect = {
  keyframes: [
    { offset: 0, opacity: 0, transform: 'translate3d(0, 100px, 0)' },
    { offset: 1, opacity: 1, transform: 'translate3d(0, 0, 0)' },
  ],
  options: {
    duration: 800,
    easing: 'ease',
    fill: 'both',
    id: 'reveal',
  },
}

function Wrapper() {
  const animate = useIntersectionAnimation({
    effect: reveal,
    observerOptions: {
      rootMargin: '-100px',
    },
  })

  return (
    <>
      <div ref={animate}>Element 1</div>
      <div ref={animate}>Element 2</div>
      <div ref={animate}>Element 3</div>
    </>
  )
}

Usage with SSR frameworks

If you are using a framework that relies on React Server Components, put the 'use client' directive at the top of the file that uses this hook.

Tracking elements

The hook returns a Ref callback to track elements. To animate an element, just pass that Ref callback to the element's ref prop.

const animate = useIntersectionAnimation()

return <div ref={animate}>Element</div>

If the target element already has a Ref attached to it, pass the values manually, like so:

const animate = useIntersectionAnimation()

const ref = useRef<HTMLDivElement | null>(null)

return (
  <div
    ref={instance => {
      ref.current = instance
      animate(instance)
    }}
  >
    Element
  </div>
)

Creating effects

This hook animates elements via the Web Animations API, which accepts a Keyframe Effect. Keyframe Effects are objects with keyframes and timing options describing an animation.

A keyframe can be written either as an array or an object. For example, to animate the opacity of an element, one could write a keyframe like this:

import { type SyntheticEffect } from '@wasamistake/use-intersection-animation'

const effectWithKeyframesArray: SyntheticEffect = {
  keyframes: [
    { offset: 0, opacity: 0 },
    { offset: 1, opacity: 1 },
  ],
  options: {
    duration: 1000,
    id: 'my-effect',
  },
}

// or

const effectWithKeyframesObject: SyntheticEffect = {
  keyframes: {
    opacity: [0, 1],
  },
  options: {
    duration: 1000,
    id: 'my-effect',
  },
}

Keyframe Effects with array syntax are much like CSS Keyframes, apart from small adaptations to make them usable with JavaScript, e.g., {offset: 0.7, ...} takes the place of 70% {...} when describing the state of an element at the animation's 70% mark.

The object syntax allows one to describe simple animations in a straightforward way, while the array syntax looks more adequate (and easier) for elaborate animations.

If you are new to those concepts, searching for 'CSS Keyframes' on YouTube will wrap your head around them.

Extending built-in effects

This package comes with some ready-to-use effects (most of them adapted from the awesome Animista library). You can import them from @wasamistake/use-intersection-animation/effects.

import {
  slideLeft,
  scaleHorizontally,
  rotateRight,
  /* ... */
} from '@wasamistake/use-intersection-animation/effects'

const animate = useIntersectionAnimation({
  effect: slideLeft,
})

The built-in effects are written with keyframe object syntax, so they can be easily extended:

import { reveal } from '@wasamistake/use-intersection-animation/effects'

const customReveal = {
  keyframes: {
    ...reveal.keyframes,
    transform: ['translate3d(0, 100%, 0)', 'translate3d(0, 0, 0)'],
  },
  options: {
    ...reveal.options,
    duration: 1000,
    id: 'my-reveal',
  },
}

const animate = useIntersectionAnimation({
  effect: customReveal,
})

Options

Below is a list of currently available hook options.

NameDescriptionDefault value
effectAn object with keyframes and timing options describing a Web Animation. Defaults to a built-in reveal animation.reveal
repeatWhether the animation should play every time the target element intersects.false
staggerSets different delays (given a multiplier in milliseconds) on animations when multiple intersections happen at the same time.100
observerOptionsOptions for the observer: root, rootMargin, and threshold.-

Options marked with * are required.

More on the observerOptions

The observer options are:

  • root: check the examples section below for more details.

  • rootMargin: a CSS-like string describing the margin of the root element. For example: -100px 10% -200px, -10vh.

  • threshold: how much of the element (in percentage) should be intersecting with the root for the animation to play. The number 0.1 means 10%, 0.3 means 30%, and so on.

You'll probably never use the root option, but the other ones will come very handy to trigger animations at the right moment, particularly with negative values for rootMargin and values greater than 0 for threshold.

Learn more about the Intersection Observer API at: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API.

Notes

  • Both the Intersection Observer and Web Animations APIs have good browser support, but if you need to deal with very old browsers, polyfills will be needed.

  • Mixing repeat: true with rootMargin < 0 or threshold > 0 can produce odd results. A default behavior for that hasn't been defined yet.

Example: Custom root element

The root is an element whose observed descendants will intersect with. It defaults to the browser viewport.

When using a custom root element, you can think of it as a mini viewport inside your page. That is, a scrollable (overflow: auto) element with descendants inside.

For this hook, specifically, the root property should be a function that returns the custom root element.

function Wrapper() {
  const animate = useIntersectionAnimation({
    observerOptions: {
      root: () => document.getElementById('custom-root'),
    },
  })

  return (
    <div id='custom-root' style={{ overflow: 'auto' }}>
      <div ref={animate}>Element 1</div>
      <div ref={animate}>Element 2</div>
    </div>
  )
}

Contributing

Found something out of place or have an idea? Please open an issue, and let's discuss that.

Remember to look for duplicates before ;)

0.2.0

2 months ago

0.1.0

2 months ago