Latticereference

Motion

Presence, response and feedback hooks, ready-made recipes, target contracts and a motion policy.

@lattice-ui/react-motionStable directiondepends on runtime, @rbxts/services

Motion 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;
Disabling motion app-wide
<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>;
Animating a surface in and out
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.

PropTypeDescription
present *booleanWhether the content should be shown. Drives reveal and exit.
config *PresenceMotionConfigThe reveal/exit recipe describing initial, reveal, and exit steps.
readybooleanGates the reveal. While false the controller holds in the preparing phase.
forceMountbooleanKeeps the instance mounted regardless of phase, so you control unmounting.
onExitComplete() => voidCalled once the exit animation completes.

useResponseMotion

function useResponseMotion<T extends Instance>(
active: boolean,
properties: MotionStateTargets,
config?: ResponseMotionConfig,
): React.MutableRefObject<T | undefined>;
Settling between two states
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>;
A press accent
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.

PropTypeDescription
appearanceroleOnly direct visual properties (colors, transparencies, Rotation). The default role.
offset-wrapperroleAn isolated wrapper whose Position is motion-owned. Adds Position.
size-wrapperroleAn isolated wrapper whose Size is motion-owned. Adds Size.
layoutroleThe component has delegated layout. Adds AnchorPoint, Position, and Size.
customroleNothing 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 using GroupTransparency, for a CanvasGroup you supply yourself. No primitive renders one. Dialog.Content, whose host spans the layer, wants a Position-only config instead.
  • createOverlayFadeRecipe — a plain backdrop fade (to half transparency on reveal).
  • createPopperEntranceRecipe and createCanvasGroupPopperEntranceRecipe — directional entrances offset by placement, for anchored popovers. The first matches the frame every anchored part renders. The second is for a canvasgroup passed in through asChild.
  • createIndicatorRevealRecipe — grows an indicator from zero to size with 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.