0.2.1 • Published 3 years ago

animate-presence v0.2.1

Weekly downloads
224
License
MIT
Repository
github
Last release
3 years ago

Unlike most animation libraries, there's no new API to learnjust use CSS or the Web Animations API.

Here's a basic example using CSS. See Declarative Animations.

<animate-presence>
  <div class="item">Item A</div>
  <div class="item">Item B</div>
  <div class="item">Item C</div>
</animate-presence>

<style>
  .item[data-enter] {
    animation: fade 1s ease-in;
  }
  .item[data-exit] {
    animation: fade 1s ease-out reverse;
    animation-fill-mode: both;
  }
  @keyframes fade {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
</style>

Here's a basic example using WAAPI. See Imperative Animations.

import { createPresenceHandler } from 'animate-presence';

const List = () => {
  const fade = new Animation({
    opacity: [0, 1],
  });
  const handleEnter = createPresenceHandler(async el => {
    return el.animate(fade, {
      duration: 1000,
      easing: 'ease-in',
      fill: 'both',
    });
  });
  const handleExit = createPresenceHandler(async el => {
    return el.animate(fade, {
      duration: 1000,
      easing: 'ease-out',
      direction: 'reverse',
      fill: 'both',
    });
  });
  return (
    <animate-presence
      onAnimatePresenceEnter={handleEnter}
      onAnimatePresenceExit={handleExit}
    >
      <div class="item">Item A</div>
      <div class="item">Item B</div>
      <div class="item">Item C</div>
    </animate-presence>
  );
};

Declarative Animations (CSS)

As child elements are added or removed, data-enter and data-exit attributes are automatically applied.

Using CSS, you can use these attributes as hooks to apply an animation or transition.

Once the animation finishes, <animate-presence> automatically cleans itself up, removing the attribute and any listeners.

Attributes

AttributeDescription
data-enterApplied immediately when a node enters. Trigger your entrance animation here.
data-exitApplied when a node will be removed. Trigger your exit animations here. The node will be removed when the animation completes.

Note Animate Presence overrides the default animation-fill-mode of animating children to both (rather than none). This provides more reasonable default behavior in most situations, so the rule is injected with a specificity (021) high enough to override the animation shorthand for most rules (like .item[data-enter]).

If you experience visual flickering at the start or end of an animation, you may be using a selector with a higher specificity than 021. The animation shorthand resets the animation-fill-mode to none, so you likely need to manually specify both.

Stagger

When multiple elements enter/exit, Animate Presence automatically sets a custom property, --i, on each child.

This makes staggered animations simple with a small calc function.

[data-enter] {
  animation-delay: calc(var(--i, 0) * 50ms);
}

Imperative Animations (WAAPI)

Using the Web Animations API to imperatively animate entrances/exits is the best way to coordinate complex, multi-step animations.

It's also possible to use your favorite animation library, like Popmotion or Anime.js!

Events

EventDescription
animatePresenceEnterDispatched immediately when a node enters. Handle your entrance animation here.
animatePresenceExitDispatched when a node will be removed. Handle your exit animations here. The node will be removed when the handler returns.

As you might expect, these events are dispatched to the children of animate-presence as they enter or exit. These events bubble, so listeners can be attached directly to an animate-presence element via addEventListener or onAnimatePresenceEnter/Exit handlers.

createPresenceHandler

Use the included createPresenceHandler utility to create async callbacks for these events.

import { createPresenceHandler } from 'animate-presence';

const onExit = createPresenceHandler(async (el, { i }) => {
  // Note that `element.animate()` doesn't return a Promise, it returns an `Animation`.
  // In order to await the `Animation`, you want `Animation.finished`.
  await el.animate(
    [{ transform: 'translateY(0)' }, { transform: 'translateY(50%)' }],
    {
      duration: 300,
      delay: i * 50,
      easing: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
      fill: 'both', // Be sure to set this!
    }
  ).finished;

  // `createPresenceHandler` can optionally `return el.animate()` instead of using `await el.animate().finished`
  await el.animate(
    [
      { opacity: 1, transform: `translate(0, 50%)` },
      { opacity: 0, transform: `translate(16px, 50%)` },
    ],
    {
      duration: 300,
      easing: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
      fill: 'both', // Be sure to set this!
    }
  ).finished;
});

const ap = document.querySelector('animate-presence');
ap.addEventListener('animatePresenceExit', onExit);

Nesting

Animate Presence uses a tree-based approach, meaning that nested animate-presence elements are aware of their parents and children.

Enter animations are applied top-down, meaning the top-level parent enters, which triggers the next child entrance, and so on.

Exit animations are applied bottom-up, meaning the deepest child exits, which triggers the next parent exit, and so on.

Animate Presence relies on querySelectorAll to construct the internal tree, so it does not (yet) work with Shadow Roots. This is an area I'm actively looking into.

Usage with @stencil/router

For Stencil apps using @stencil/router, Animate Presence has an <animated-route-switch> component which allows you to smoothly animate between routes.

  1. Swap your <stencil-route-switch> component with <animated-route-switch>

  2. Due to a current bug in @stencil/router, you will need to use injectHistory to pass location down to animated-route-switch.

    Hopefully this will be fixed soon, but there's a simple work around for now.

import { Component, h, Prop } from '@stencil/core';
import { injectHistory, LocationSegments } from '@stencil/router';

@Component({
  tag: 'app-root',
  styleUrl: 'app-root.css',
  scoped: true,
})
export class AppRoot {
  @Prop() location: LocationSegments;

  render() {
    return (
      <div>
        <header>
          <h1>Stencil App Starter</h1>
        </header>

        <main>
          <stencil-router>
            {/* Pass `location` to animated-route-switch */}
            <animated-route-switch location={this.location}>
              {/* <animate-presence> should exist somewhere within these components */}
              <stencil-route url="/" exact={true} component="app-home" />
              <stencil-route url="/profile/:name" component="app-profile" />
            </animated-route-switch>
          </stencil-router>
        </main>
      </div>
    );
  }
}
injectHistory(AppRoot);
  1. Apply your animations on [data-enter] and [data-exit] as usual.

As noted above, if your Stencil components rely on shadow encapsulation, Animate Presence won't work as expected (yet). I'm working on it. For now, you can either use scoped encapsulation or stay tuned for updates!

0.0.0-e90c470

3 years ago

0.0.0-223174c

3 years ago

0.0.0-fa24a00

4 years ago

0.0.0-b119969

4 years ago

0.0.0-2062c4d

4 years ago

0.0.0-b1992ae

4 years ago

0.0.0-4adf64f

4 years ago

0.0.0-379153f

4 years ago

0.0.0-5a2454a

4 years ago

0.2.1

4 years ago

0.2.1-rc.47d491d

4 years ago

0.2.1-rc.c9a43fa

4 years ago

0.0.0-0e7af75

4 years ago

0.2.0

4 years ago

0.1.1

4 years ago

0.0.1

4 years ago