Animation

Xanui-core ships a small imperative animation engine (animate) plus a set of Easing presets and three React hooks built on top of it: useTransition, useTransitionGroup, and useInView. All of these are exported directly from xanui-core.

Unlike CSS-in-JS keyframe helpers, animate interpolates plain numeric values (not CSS strings) frame by frame via requestAnimationFrame, and calls your onUpdate callback with the current value on every frame so you can apply it however you like (inline styles, a ref, a CSS variable, etc.).

animate(options)

ts

import { animate, Easing } from 'xanui-core'
import type { AnimateOptions } from 'xanui-core'

animate accepts a single AnimateOptions<T> object, where T is a record of numeric values (e.g. { scale: number; opacity: number }).

OptionTypeDefaultDescription
fromT|||(()|=>|T)Starting|numeric|values|(or|a|function|that|returns|them).
toT|||(()|=>|T)Ending|numeric|values|(or|a|function|that|returns|them).
durationnumber|(ms)400Length|of|the|animation.
delaynumber|(ms)0Delay|before|the|animation|starts.
easing(t:|number)|=>|number|||Partial<Record<keyof|T,|(t:|number)|=>|number>>Easing.defaultA|single|easing|function|applied|to|every|key,|or|a|per-key|map|of|easing|functions.
onUpdate(value:|T,|progress:|number)|=>|voidCalled|on|every|frame|(including|the|first|frame|at|progress|=|0|and|the|last|at|progress|=|1)|with|the|interpolated|values.
onDone(value:|T)|=>|voidCalled|once|the|animation|(and|any|repeats)|has|finished.
breakpointsPartial<Record<keyof|T,|Array<{|value:|number;|callback:|()|=>|void|}>>>Per-key|thresholds.|When|the|interpolated|value|for|that|key|crosses|value|during|the|animation,|callback|fires|once.
repeatnumber0Number|of|additional|times|to|repeat|the|animation|after|the|first|run.
repeatBackbooleanfalseWhen|true,|each|repeat|reverses|direction|(ping-pong)|instead|of|restarting|from|from.

animate returns a cancel function: calling it clears the pending requestAnimationFrame/setTimeout and stops the animation.

tsx

import { animate, Easing } from 'xanui-core'
const cancel = animate({
from: { scale: , opacity: },
to: { scale: , opacity: },
duration: ,
easing: Easing.standard,
onUpdate: (value) => {
el.style.transform = `scale(${value.scale})`
el.style.opacity = String(value.opacity)
},
onDone: () => console.log('done'),
})
// later, to cancel mid-flight:
cancel()

Per-key easing and breakpoints

tsx

animate({
from: { x: , y: },
to: { x: , y: },
duration: ,
easing: { x: Easing.smooth, y: Easing.easeOutBounce },
breakpoints: {
x: [{ value: , callback: () => console.log('halfway across') }],
},
onUpdate: (value) => {
box.style.transform = `translate(${value.x}px, ${value.y}px)`
},
})

Easing

Easing is an object of ready-made timing functions, each with the signature (t: number) => number:

NameDescription
defaultCubic|ease-out|(1|-|(1|-|t)|**|3);|used|when|no|easing|option|is|given.
standardCubic-bezier|(0.4,|0,|0.2,|1).
fastCubic-bezier|(0.2,|0,|0,|1).
smoothCubic-bezier|(0.25,|0.46,|0.45,|0.94).
linearCubic-bezier|(0,|0,|1,|1)|(no|easing).
bounceBezierCubic-bezier|(0.34,|1.5,|0.64,|1)|(overshoot).
cubicInOutSymmetric|cubic|ease-in-out.
easeOutBounceBounce-out|(ball-drop)|easing.
springDamped-oscillation/spring-like|curve.

useTransition

useTransition<T>(props: UseTransitionProps<T>) wraps animate with enter/exit state management for a single value.

UseTransitionProps<T> = AnimateOptions<T> plus:

OptionTypeDefaultDescription
initialStatus'entered'|||'exited''exited'Whether|the|transition|starts|already|"entered"|or|"exited".
onEnter()|=>|voidCalled|synchronously|when|enter()|starts.
onEntered()|=>|voidCalled|when|the|enter|animation|completes.
onExit()|=>|voidCalled|synchronously|when|exit()|starts.
onExited()|=>|voidCalled|when|the|exit|animation|completes.

Return value:

FieldTypeDescription
isEnteredbooleantrue|once|enter()|has|been|requested|(open|state).
status'entering'|||'entered'|||'exiting'|||'exited'Current|transition|status.
stateReact.RefObject<T>Ref|holding|the|latest|interpolated|value.
enter(withAnimation?:|boolean)|=>|voidTransitions|to|the|to|value.|Pass|false|to|skip|the|animation.
exit(withAnimation?:|boolean)|=>|voidTransitions|to|the|from|value.|Pass|false|to|skip|the|animation.
toggle(withAnimation?:|boolean)|=>|voidToggles|between|enter/exit.
isReadybooleantrue|once|the|initial|value|has|been|resolved|(after|mount).

tsx

import { useTransition } from 'xanui-core'
const Collapse = ({ open, children }: { open: boolean; children: React.ReactNode }) => {
const { state, enter, exit, status } = useTransition({
from: { height: , opacity: },
to: { height: , opacity: },
duration: ,
onUpdate: (value) => {
el.current!.style.height = `${value.height}px`
el.current!.style.opacity = String(value.opacity)
},
})
useEffect(() => {
open ? enter() : exit()
}, [open])
return <div ref={el} data-status={status}>{children}</div>
}

useTransitionGroup

useTransitionGroup<T>(options: UseTransitionGroupProps<T>) runs staggered enter/exit animations across a list of keyed items.

OptionTypeDefaultDescription
itemsArray<{|key:|string|||number;|from:|T;|to:|T|}>The|items|to|animate.
durationnumber400Duration|of|each|item's|animation.
staggernumber100Delay|in|ms|added|per|item|index.
mountOnEnterbooleanWhen|true,|items|start|unmounted|and|are|mounted|right|before|entering.
unmountOnExitbooleanWhen|true,|items|are|unmounted|once|their|exit|animation|finishes.
onUpdate(value:|T,|key:|string|||number,|progress:|number)|=>|voidCalled|on|every|frame|for|every|animating|item.
onEnter(key:|string|||number)|=>|voidCalled|when|an|item|starts|entering.
onEntered(key:|string|||number)|=>|voidCalled|when|an|item|finishes|entering.
onExit(key:|string|||number)|=>|voidCalled|when|an|item|starts|exiting.
onExited(key:|string|||number)|=>|voidCalled|when|an|item|finishes|exiting.

Returns { statuses, mounted, enter, exit, toggle }, where statuses and mounted are records keyed by item key (statuses[key] is a UseTransitionStatus, mounted[key] is a boolean), and enter/exit/toggle (each () => void) run the staggered animation across all items.

tsx

import { useTransitionGroup } from 'xanui-core'
const items = list.map((item, i) => ({ key: item.id, from: { y: , opacity: }, to: { y: , opacity: } }))
const { statuses, mounted, enter } = useTransitionGroup({
items,
stagger: ,
mountOnEnter: true,
onUpdate: (value, key) => applyStyle(key, value),
})
useEffect(() => { enter() }, [])

useInView

useInView<T extends HTMLElement>(options?: UseInViewOptions) tracks whether an element is intersecting its scroll container using IntersectionObserver.

OptionTypeDefaultDescription
thresholdnumber0.1Intersection|ratio|required|to|be|considered|"in|view".
rootElement|||nullnullThe|scroll|container|to|observe|against|(null|=|viewport).
marginnumber0Added|as|rootMargin:|${margin|*|8}px``|(an|8px-per-unit|spacing|scale|value).
oncebooleanfalseWhen|true,|stops|observing|after|the|element|becomes|visible|once.

Returns { ref, inView }, where ref must be attached to the element you want to observe and inView is a boolean.

tsx

import { useInView } from 'xanui-core'
const Reveal = ({ children }: { children: React.ReactNode }) => {
const { ref, inView } = useInView({ threshold: , once: true })
return (
<div ref={ref} style={{ opacity: inView ? : , transition: 'opacity 400ms' }}>
{children}
</div>
)
}