API Overview
| Symbol | Purpose | Common gotcha |
|---|---|---|
animate() | Animate one element | Defaults to a visible 180ms duration |
animateEach() | Animate a unique element group | Non-zero stagger needs numeric delay |
captureLayout() | Capture positions and create a one-shot FLIP transition | Capture before changing layout |
NecromancerError | Base package error | Use NecromancerError.is() to narrow unknown errors |
Package Entry Point
All public functions, errors, and types are exported from @vielzeug/necromancer.
Animation Functions
animate()
function animate(element: Element, keyframes: Keyframes, options?: AnimateOptions): AnimationHandle;Starts a lifecycle-owned native Web Animation. Omitted duration defaults to 180 milliseconds; explicit native timing values, including 0, are preserved. Playback remains native:
const handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });
handle.animation.pause();
const result = await handle.result;
handle.dispose();animateEach()
function animateEach(
elements: Iterable<Element>,
keyframes: Keyframes | KeyframeFactory,
options?: AnimateEachOptions,
): AnimationGroup;Starts animations for unique elements in first-seen order. Necromancer resolves every keyframe factory before starting the first native animation. Use each child handle's animation property for native playback control.
Layout Functions
captureLayout()
function captureLayout(elements: Iterable<Element>, options?: LayoutCaptureOptions): LayoutTransition;Captures unique elements' positions and sizes and returns a one-shot transition. Rotation and other transforms are not captured or compensated. After changing layout, call transition.animate(options) to measure current positions and sizes and animate changed, connected elements with additive CSS translate (position) and scale (size). Pass getKey when a framework replaces the captured elements during its render.
const transition = captureLayout(beforeItems, {
getKey: (element) => element.getAttribute('data-id')!,
});
renderReorderedItems();
const group = transition.animate({
duration: 220,
easing: 'ease-out',
elements: afterItems,
});Calling animate() twice on the same transition throws NecromancerConfigError.
Types
MotionMode
type MotionMode = 'full' | 'reduced' | 'system';'system' is the default. Reduced motion preserves the supplied keyframes while normalizing delay, duration, and end delay to 0, and iterations to 1.
AnimationResult
type AnimationResult =
| { readonly status: 'finished' }
| { readonly status: 'reduced' }
| { readonly reason?: unknown; readonly status: 'cancelled' };cancelled describes native cancellation and includes its native rejection reason. A reason passed to dispose() or an abort signal takes precedence. The independent disposed property becomes true only when the lifecycle owner is explicitly disposed.
AnimateOptions
type AnimateOptions = KeyframeAnimationOptions & {
readonly interrupt?: 'cancel';
readonly motion?: MotionMode;
readonly signal?: AbortSignal;
};Set interrupt: 'cancel' for rapid state changes that should replace every still-active Necromancer-owned animation on the same element. It does not cancel animations created directly with Element.animate().
AnimateEachOptions
type AnimateEachOptions = AnimateOptions & {
readonly stagger?: number;
};stagger is a finite, non-negative millisecond offset.
LayoutCaptureOptions
interface LayoutCaptureOptions {
readonly getKey?: (element: Element) => string;
}getKey maps a captured element and its committed replacement to the same stable, non-empty string. Duplicate or empty keys throw NecromancerConfigError.
LayoutAnimationOptions
type LayoutAnimationOptions = AnimateEachOptions & {
readonly elements?: Iterable<Element>;
};elements is the collection in its committed layout. Omit it to animate the same captured elements. With getKey, replacement elements animate from the positions of their captured predecessors. Unmatched, removed, and newly entered elements are ignored.
Keyframes and KeyframeFactory
type Keyframes = readonly Keyframe[] | PropertyIndexedKeyframes;
type KeyframeFactory = (element: Element, index: number, total: number) => Keyframes;Accepts a readonly array so a reusable as const keyframe list can be passed without a cast.
AnimationHandle
interface AnimationHandle {
readonly animation: Animation;
readonly result: Promise<AnimationResult>;
readonly disposed: boolean;
dispose(reason?: unknown): void;
[Symbol.dispose](): void;
}AnimationGroup
interface AnimationGroup {
readonly handles: readonly AnimationHandle[];
readonly results: Promise<readonly AnimationResult[]>;
readonly disposed: boolean;
dispose(reason?: unknown): void;
[Symbol.dispose](): void;
}results preserves the terminal result of every child in handle order. Use handles for native playback control.
LayoutTransition
interface LayoutTransition {
animate(options?: LayoutAnimationOptions): AnimationGroup;
}Errors
| Error | Trigger |
|---|---|
NecromancerError | Base class for package errors |
NecromancerConfigError | Invalid stagger, incompatible delay, or reused layout transition |
NecromancerUnsupportedError | Element.animate() is unavailable |
Testing (@vielzeug/necromancer/testing)
jsdom (and most non-browser DOM environments) do not implement Element.animate(). Import these from the /testing sub-path, not the root entry point.
installFakeAnimations()
function installFakeAnimations(): { calls: AnimationCall[]; restore: () => void };Replaces Element.prototype.animate with a deterministic fake for the duration of a test. calls records every invocation in order; call restore() (for example in afterEach) to put the original implementation back.
import { installFakeAnimations } from '@vielzeug/necromancer/testing';
const { calls, restore } = installFakeAnimations();
const handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);
calls[0]?.animation.finish();
await handle.result; // { status: 'finished' }
restore();FakeAnimation
class FakeAnimation {
cancelCallCount: number;
finishCallCount: number;
finished: Promise<void>;
cancel(): void;
finish(): void;
}A minimal Animation stand-in. cancel() rejects finished with an AbortError; finish() resolves it. cancelCallCount/finishCallCount track how many times each was called, in place of a test-runner-specific spy.
createRect()
function createRect(x: number, y: number, width?: number, height?: number): DOMRect;Builds a DOMRect for mocking Element.getBoundingClientRect() in captureLayout() tests. width/height default to 20.