animaly
Documentation menu

Guides

Gestures

hover, press, inView and resize start your code when the pointer, the keyboard, the viewport or a size changes. Each takes elements or a selector, and each returns a function that removes its listeners.

Start and end

Every gesture calls your function with the element when it starts. Return a function to be told when it ends: the pointer leaves, the press is released, the element scrolls out of view. The gestures only listen; what moves is up to the animate calls you write.

Hover and press on the same element
import { animate, hover, press } from "@animaly/dom";

const spring = { type: "spring", stiffness: 400, damping: 25 } as const;

hover(".button", (element) => {
  animate(element, { scale: 1.04 }, spring);
  return () => animate(element, { scale: 1 }, spring);
});

press(".button", (element) => {
  animate(element, { scale: 0.96 }, spring);
  return () => animate(element, { scale: 1.04 }, spring);
});

hover

hover starts when a mouse or pen enters the element and ends when it leaves. Touch input is ignored, so a tap on a phone doesn't leave an element stuck in its hover state.

Scale up on hover
import { animate, hover } from "@animaly/dom";

hover(".button", (element) => {
  animate(element, { scale: 1.05 }, { type: "spring", stiffness: 400, damping: 25 });
  return () => animate(element, { scale: 1 }, { type: "spring", stiffness: 400, damping: 25 });
});
Tilt toward where the pointer entered
import { animate, hover } from "@animaly/dom";

hover(".card", (element, event) => {
  const box = element.getBoundingClientRect();
  const side = event.clientX < box.left + box.width / 2 ? -1 : 1;
  animate(element, { rotate: side * 3, y: -4 }, { type: "spring" });
  return () => animate(element, { rotate: 0, y: 0 }, { type: "spring" });
});
Pause a looping animation while hovered
import { animate, hover } from "@animaly/dom";

const spin = animate(".tile", { rotate: [0, 360] }, { duration: 6, ease: "linear", repeat: Infinity });

hover(".tile", () => {
  spin.pause();
  return () => spin.play();
});

If the pointer leaves while a button is held down, the end waits until the button is released.

Remove the hover listeners
import { animate, hover } from "@animaly/dom";

const stop = hover(".button", (element) => {
  animate(element, { opacity: 0.8 });
  return () => animate(element, { opacity: 1 });
});

stop();

press

press starts on the primary pointer going down. The end receives the release event and success, which is true when the pointer came up on the element and false when it came up elsewhere or the press was cancelled.

Press down and spring back
import { animate, press } from "@animaly/dom";

const spring = { type: "spring", stiffness: 500, damping: 30 } as const;

press(".button", (element) => {
  animate(element, { scale: 0.95 }, spring);
  return () => animate(element, { scale: 1 }, spring);
});
Only act when released on the element
import { animate, press } from "@animaly/dom";

press(".button", (element) => {
  animate(element, { scale: 0.95 });
  return (_event, { success }) => {
    animate(element, { scale: 1 });
    if (success) console.log("saved");
  };
});

Keyboard

A focused element presses with Enter. Elements that can't take focus, such as a div without a tabindex, get tabIndex = 0 so keyboard users can reach them.

Pressable cards that work with the keyboard
import { animate, press } from "@animaly/dom";

press(".card", (element) => {
  animate(element, { scale: 0.97 }, { duration: 0.1 });
  return () => animate(element, { scale: 1 }, { type: "spring" });
});

console.log(document.querySelector<HTMLElement>(".card")!.tabIndex); // 0

Options

Nested pressables
import { animate, press } from "@animaly/dom";

press(".button", (element) => {
  animate(element, { scale: 0.9 });
  return () => animate(element, { scale: 1 });
}, { stopPropagation: true });

press(".card", (element) => {
  animate(element, { scale: 0.98 });
  return () => animate(element, { scale: 1 });
});
React to the first press only
import { animate, press } from "@animaly/dom";

const stop = press(".button", (element) => {
  stop();
  animate(element, { opacity: 0.5 });
});
press options
OptionTypeDefaultWhat it does
useGlobalTargetbooleanfalseListen on the window instead: any primary press on the page starts it, the start receives the window, and every release counts as success.
stopPropagationbooleanfalseClaim the press so pressable ancestors don't start too.
passivebooleantruePassed to addEventListener.
oncebooleanfalsePassed to addEventListener.

inView

inView starts when the element scrolls into view. Without a returned function it fires once per element and stops watching it. With one, it reports leaving and starts again on the next entry.

Fade sections in once
import { animate, inView } from "@animaly/dom";

inView(".reveal", (element) => {
  animate(element, { opacity: 1, y: [24, 0] }, { duration: 0.5, ease: "easeOut" });
});
Play on enter, reverse on leave
import { animate, inView } from "@animaly/dom";

inView(".reveal", (element) => {
  animate(element, { opacity: 1 });
  return () => animate(element, { opacity: 0 });
});
Wait until half of the element is visible
import { animate, inView } from "@animaly/dom";

inView(".reveal", (element) => {
  animate(element, { opacity: 1 });
}, { amount: 0.5 });

inView(".reveal", (element) => {
  animate(element, { scale: [0.95, 1] });
}, { amount: "all" });
Start a little before it scrolls in
import { animate, inView } from "@animaly/dom";

inView(".reveal", (element) => {
  animate(element, { opacity: 1 });
}, { margin: "0px 0px 200px 0px" });
Inside a scrolling list
import { animate, inView } from "@animaly/dom";

inView(".row", (element) => {
  animate(element, { opacity: 1, x: [-16, 0] }, { type: "spring" });
}, { root: document.querySelector(".list")! });
Stagger children when the parent appears
import { animate, inView, stagger } from "@animaly/dom";

inView(".list", (element) => {
  animate(element.querySelectorAll(".row"), { opacity: 1, y: [12, 0] }, { delay: stagger(0.06) });
});
Play a video only while it is on screen
import { inView } from "@animaly/dom";

inView(".clip", (element) => {
  const video = element as HTMLVideoElement;
  video.play().catch(() => {});
  return () => video.pause();
});
inView options
OptionTypeDefaultWhat it does
rootElement | Documentthe viewportThe scrolling element to measure visibility against.
marginstringnoneGrows or shrinks the root's box, in CSS margin syntax.
amount"some" | "all" | number"some"How much of the element must be visible: any part, all of it, or a fraction from 0 to 1.

resize

With a target, resize calls your function with the element and its border-box size, once right away and again on every change. With only a function, it follows the window and reports innerWidth and innerHeight on each resize event.

React to an element's size
import { animate, resize } from "@animaly/dom";

resize(".panel", (element, { width }) => {
  const bar = element.querySelector(".bar")!;
  animate(bar, { scaleX: Math.min(1, width / 600) }, { type: "spring" });
});
React to the window size
import { resize } from "@animaly/dom";

const stop = resize(({ width, height }) => {
  document.documentElement.style.setProperty("--aspect", String(width / height));
});

stop();

In components

Call the gestures in an effect and return their stop functions from it.

In a React component
import { animate, hover, press } from "@animaly/dom";
import { useEffect, useRef } from "react";

export function PressableCard({ children }: { children: React.ReactNode }) {
  const card = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const element = card.current;
    if (!element) return;
    const stopHover = hover(element, () => {
      animate(element, { y: -4 }, { type: "spring" });
      return () => animate(element, { y: 0 }, { type: "spring" });
    });
    const stopPress = press(element, () => {
      animate(element, { scale: 0.98 }, { duration: 0.1 });
      return () => animate(element, { scale: 1 }, { type: "spring" });
    });
    return () => {
      stopHover();
      stopPress();
    };
  }, []);

  return <div ref={card}>{children}</div>;
}

animaly has no drag gesture yet. For drag and throw, track the pointer yourself and hand the release velocity to an inertia animation.