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.
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.
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 });
});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" });
});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.
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.
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);
});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.
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); // 0Options
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 });
});import { animate, press } from "@animaly/dom";
const stop = press(".button", (element) => {
stop();
animate(element, { opacity: 0.5 });
});| Option | Type | Default | What it does |
|---|---|---|---|
| useGlobalTarget | boolean | false | Listen on the window instead: any primary press on the page starts it, the start receives the window, and every release counts as success. |
| stopPropagation | boolean | false | Claim the press so pressable ancestors don't start too. |
| passive | boolean | true | Passed to addEventListener. |
| once | boolean | false | Passed 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.
import { animate, inView } from "@animaly/dom";
inView(".reveal", (element) => {
animate(element, { opacity: 1, y: [24, 0] }, { duration: 0.5, ease: "easeOut" });
});import { animate, inView } from "@animaly/dom";
inView(".reveal", (element) => {
animate(element, { opacity: 1 });
return () => animate(element, { opacity: 0 });
});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" });import { animate, inView } from "@animaly/dom";
inView(".reveal", (element) => {
animate(element, { opacity: 1 });
}, { margin: "0px 0px 200px 0px" });import { animate, inView } from "@animaly/dom";
inView(".row", (element) => {
animate(element, { opacity: 1, x: [-16, 0] }, { type: "spring" });
}, { root: document.querySelector(".list")! });import { animate, inView, stagger } from "@animaly/dom";
inView(".list", (element) => {
animate(element.querySelectorAll(".row"), { opacity: 1, y: [12, 0] }, { delay: stagger(0.06) });
});import { inView } from "@animaly/dom";
inView(".clip", (element) => {
const video = element as HTMLVideoElement;
video.play().catch(() => {});
return () => video.pause();
});| Option | Type | Default | What it does |
|---|---|---|---|
| root | Element | Document | the viewport | The scrolling element to measure visibility against. |
| margin | string | none | Grows 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.
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" });
});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.
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.