@lattice-ui/react-motionStable directiondepends on runtime, @rbxts/servicesMotion is the animation foundation for Lattice UI. It works in terms of intent, a tempo and tone, and target contracts, which properties motion may own on an instance. Three hooks cover the three domains: presence for mount and unmount transitions, response for state-to-state settling, and feedback for momentary accents. A library of recipes encodes the house style for common surfaces.
Dialog, Popover, Menu, Tooltip, Toast, Slider and Switch all animate through these hooks. The whole system respects a global policy, so motion can be reduced or disabled app-wide.
Import
import { MotionProvider, useMotionPolicy, usePresenceMotion, usePresenceMotionController, useResponseMotion, useFeedbackEffect, motionTargets, createSurfaceRevealRecipe,} from "@lattice-ui/react-motion";API reference
MotionProvider
function MotionProvider(props: { mode?: "full" | "none"; disableAllMotion?: boolean; children: React.ReactNode;}): React.Element;<MotionProvider disableAllMotion={reducedMotionSetting}> <App /></MotionProvider>Sets the ambient motion policy for the subtree. With mode="full", the default, animations run
normally. With mode="none", or disableAllMotion set, every hook snaps to its goal values. Wrap
it near your app root and bind it to a player’s reduced-motion preference.
useMotionPolicy
function useMotionPolicy(): { mode: "full" | "none"; disableAllMotion: boolean };Reads the current motion policy, defaulting to { mode: "full", disableAllMotion: false } with no
provider. Call it directly only when your own component needs to branch on whether motion is
enabled.
usePresenceMotion
function usePresenceMotion<T extends Instance>( present: boolean, config: PresenceMotionConfig, onExitComplete?: () => void,): React.MutableRefObject<T | undefined>;const ref = usePresenceMotion<Frame>(open, createSurfaceRevealRecipe(), () => unmount());
return <frame ref={ref} Size={UDim2.fromOffset(320, 180)} />;The simplest presence entry point: attach the returned ref to the instance, flip present to drive
reveal and exit, and pass onExitComplete for when the exit finishes. It wraps
usePresenceMotionController with forceMount set, so the instance stays mounted while you handle
unmounting yourself.
usePresenceMotionController
function usePresenceMotionController<T extends Instance>( options: PresenceMotionControllerOptions,): PresenceMotionController<T>;
interface PresenceMotionControllerOptions { present: boolean; config: PresenceMotionConfig; ready?: boolean; forceMount?: boolean; onExitComplete?: () => void;}
interface PresenceMotionController<T extends Instance> { ref: React.MutableRefObject<T | undefined>; phase: PresenceMotionPhase; mounted: boolean; ready: boolean; present: boolean; isExiting: boolean; isVisible: boolean; markReady: () => void;}The full presence controller. It exposes the lifecycle. Gate the reveal on layout being measured
with ready and markReady, keep an instance mounted across its exit with forceMount, and read
phase, isExiting, isVisible and mounted. With motion disabled, the reveal and exit snapshots
apply instantly. phase advances through exited, mounted, preparing, ready, visible and
exiting.
| Prop | Type | Description |
|---|---|---|
| present * | boolean | Whether the content should be shown. Drives reveal and exit. |
| config * | PresenceMotionConfig | The reveal/exit recipe describing initial, reveal, and exit steps. |
| ready | boolean | Gates the reveal. While false the controller holds in the preparing phase. |
| forceMount | boolean | Keeps the instance mounted regardless of phase, so you control unmounting. |
| onExitComplete | () => void | Called once the exit animation completes. |
useResponseMotion
function useResponseMotion<T extends Instance>( active: boolean, properties: MotionStateTargets, config?: ResponseMotionConfig,): React.MutableRefObject<T | undefined>;const ref = useResponseMotion<Frame>(checked, { active: { Position: UDim2.fromScale(1, 0) }, inactive: { Position: UDim2.fromScale(0, 0) },});Animates an instance between two property sets as active toggles. The first mount applies the
current state instantly, and subsequent toggles settle using config.settle. This is the hook
behind toggles, sliders, selection indicators and progress fills.
useFeedbackEffect
function useFeedbackEffect<T extends Instance>( active: boolean, properties: MotionStateTargets, config?: FeedbackEffectConfig,): React.MutableRefObject<T | undefined>;const ref = useFeedbackEffect<TextButton>(pressed, { active: { BackgroundTransparency: 0.1 }, inactive: { BackgroundTransparency: 0.2 },}, createPressFeedbackEffect());Applies a momentary accent/recover effect as active toggles. Use it for press, hover and focus
emphasis, where the motion is a brief reaction rather than a persistent state change.
MotionStateTargets
type MotionStateTargets = { active: MotionProperties; inactive: MotionProperties;};
type MotionProperties = Record<string, unknown>;The two-state property bag consumed by useResponseMotion and useFeedbackEffect: the values to apply when active is true and when it is false.
Target contracts
Motion only writes properties an instance has delegated to it. A MotionTargetContract declares
that boundary by role, with optional allow/deny lists.
type MotionTargetRole = | "appearance" | "offset-wrapper" | "size-wrapper" | "layout" | "custom";
type MotionTargetContract = { role: MotionTargetRole; label?: string; allowProperties?: Array<string>; denyProperties?: Array<string>;};
function createMotionTargetContract( role: MotionTargetRole, options?: { label?: string; allowProperties?: Array<string>; denyProperties?: Array<string> },): MotionTargetContract;
const motionTargets: { appearance: (label?: string) => MotionTargetContract; offsetWrapper: (label?: string) => MotionTargetContract; sizeWrapper: (label?: string) => MotionTargetContract; layout: (label?: string) => MotionTargetContract; custom: (options: { label?: string; allowProperties?: Array<string>; denyProperties?: Array<string> }) => MotionTargetContract;};Each role permits a different set of properties. Appearance properties are always allowed: colours,
transparencies and Rotation.
| Prop | Type | Description |
|---|---|---|
| appearance | role | Only direct visual properties (colors, transparencies, Rotation). The default role. |
| offset-wrapper | role | An isolated wrapper whose Position is motion-owned. Adds Position. |
| size-wrapper | role | An isolated wrapper whose Size is motion-owned. Adds Size. |
| layout | role | The component has delegated layout. Adds AnchorPoint, Position, and Size. |
| custom | role | Nothing is owned by default; only properties listed in allowProperties. |
Use motionTargets.* for the common cases and createMotionTargetContract when you need explicit
allow/deny lists. Reserved properties such as AbsolutePosition, AutomaticSize, LayoutOrder,
Parent, Visible and ZIndex are never writable.
Intent and config types
type MotionTempo = "instant" | "swift" | "steady" | "gentle";type MotionTone = "calm" | "responsive" | "expressive";type MotionIntent = { tempo?: MotionTempo; tone?: MotionTone; duration?: number };
type MotionStep = { values: MotionProperties; intent?: MotionIntent; target?: MotionTargetContract };
interface PresenceMotionConfig { target?: MotionTargetContract; initial?: MotionProperties; reveal?: MotionStep; exit?: MotionStep;}
interface ResponseMotionConfig { target?: MotionTargetContract; settle?: MotionIntent;}
interface FeedbackEffectConfig { target?: MotionTargetContract; accent?: MotionIntent; recover?: MotionIntent;}MotionIntent expresses how something moves, through a tempo, a tone and an optional explicit
duration, rather than raw easing curves. Presence has initial, reveal and exit steps.
Response has a single settle intent, and feedback has accent and recover.
Other exported types include MotionDomain ("presence" | "response" | "feedback"), PresenceMotionPhase, MotionTargetContractOptions, and MotionPolicy.
Presence recipes
Ready-made PresenceMotionConfig builders encoding the house style, each taking optional tuning
parameters.
function createSurfaceRevealRecipe(offsetY?: number, duration?: number): PresenceMotionConfig;function createCanvasGroupRevealRecipe(offsetY?: number, duration?: number): PresenceMotionConfig;function createOverlayFadeRecipe(duration?: number): PresenceMotionConfig;function createPopperEntranceRecipe(placement?: MotionPlacement, distance?: number, duration?: number): PresenceMotionConfig;function createCanvasGroupPopperEntranceRecipe(placement?: MotionPlacement, distance?: number, duration?: number): PresenceMotionConfig;function createIndicatorRevealRecipe(size: UDim2, duration?: number): PresenceMotionConfig;createSurfaceRevealRecipe— a small upward slide plus background fade for opaque surfaces.createCanvasGroupRevealRecipe— the same slide usingGroupTransparency, for aCanvasGroupyou supply yourself. No primitive renders one.Dialog.Content, whose host spans the layer, wants aPosition-only config instead.createOverlayFadeRecipe— a plain backdrop fade (to half transparency on reveal).createPopperEntranceRecipeandcreateCanvasGroupPopperEntranceRecipe— directional entrances offset byplacement, for anchored popovers. The first matches theframeevery anchored part renders. The second is for acanvasgrouppassed in throughasChild.createIndicatorRevealRecipe— grows an indicator from zero tosizewith a fade, for selection/check indicators.
Response recipes
ResponseMotionConfig builders for state-to-state settling:
function createIndicatorSettleRecipe(duration?: number): ResponseMotionConfig;function createSliderThumbResponseRecipe(isDragging: boolean, duration?: number): ResponseMotionConfig;function createToggleResponseRecipe(duration?: number): ResponseMotionConfig;function createSelectionResponseRecipe(duration?: number): ResponseMotionConfig;function createFieldResponseRecipe(duration?: number): ResponseMotionConfig;function createProgressResponseRecipe(duration?: number): ResponseMotionConfig;function createToastResponseRecipe(duration?: number): ResponseMotionConfig;createSliderThumbResponseRecipe tightens to an instant tempo while dragging. The rest map their
named surface to a tuned settle intent.
Feedback recipes
FeedbackEffectConfig builders for momentary accents:
function createPressFeedbackEffect(duration?: number): FeedbackEffectConfig;function createFocusAccentEffect(duration?: number): FeedbackEffectConfig;createPressFeedbackEffect gives an expressive accent on press and a calm recover.
createFocusAccentEffect does the same for focus emphasis, with a slightly longer accent.
Target utilities
Lower-level helpers over motion property values and instances, used by the runtime. They respect
target contracts via an optional MotionPropertyContext.
function validateMotionProperty(instance: Instance, key: string, context?: MotionPropertyContext): boolean;function readMotionProperty(instance: Instance, key: string, context?: MotionPropertyContext): unknown;function writeMotionProperty(instance: Instance, key: string, value: unknown, context?: MotionPropertyContext): boolean;function applyMotionProperties(instance: Instance, values?: MotionProperties, context?: MotionPropertyContext): void;function areMotionValuesEqual(a: unknown, b: unknown, precision?: number): boolean;function canInterpolateMotionValue(from: unknown, to: unknown): boolean;function interpolateMotionValue(from: unknown, to: unknown, alpha: number, context?: MotionPropertyContext & { propertyKey?: string }): unknown;function isMotionValueSettled(current: unknown, target: unknown, precision?: number): boolean;They validate that a property is motion-owned before reading or writing it, and interpolate
number, UDim, UDim2, Vector2, Vector3, Color3 and CFrame. Reach for them only when
implementing a custom motion host.
Placement offsets
type MotionPlacement = "top" | "bottom" | "left" | "right";
function createPlacementOffset(placement: MotionPlacement | undefined, distance: number): UDim2;Builds the directional UDim2 offset a surface animates from, given a placement and pixel distance.
The popper entrance recipes use it to slide content in from the correct side.
Every write goes through a target contract. Animating a property the contract does not own is rejected with a diagnostic, such as Position under an appearance role. Move geometry into an isolated wrapper, or use a layout target.
With disableAllMotion, or mode="none", the hooks apply their target snapshots immediately and still fire completion callbacks. Lifecycle behaviour is identical, and only the in-between animation is skipped.