@lattice-ui/motion is the shared place for animation behavior in Lattice UI.
When a primitive needs transition timing, reduced-motion handling, enter/exit
state, or interpolation, it should use the motion package instead of owning a
private tween model.
How to apply it
Use System for app-wide theme and density, Layer
for overlay mounting and stacking, Focus for trapped scopes
and restoration, and Motion for animated state. Keeping those ownership lines
clear prevents Dialog, Popover, Select, Tooltip, and Toast from each
inventing different rules for the same lifecycle.
- Use presence motion when content appears and disappears, especially portal content and indicators with exit transitions.
- Use response motion when a mounted control settles between states, such as a Slider thumb, Switch thumb, or selected item background.
- Use feedback motion for brief press or focus accents.
- Put reduced-motion policy behind
MotionProviderso components can respect one shared policy.
For layered components, animate a visual wrapper rather than the placement owner. Popover, Select, and Tooltip still need placement to come from Popper; Motion should make the content feel responsive without invalidating the anchor geometry.
Consumer guidance
For Dialog, keep overlay fade and panel reveal as separate motion concerns. The dialog owns open state, dismissal, focus trapping, and focus restoration; Motion owns the visual transition while those semantics stay active.
For Popover, Select, and Tooltip, let the content remain mounted while exit motion runs. Do not unmount portal content immediately on close if the user expects an exit transition.
For Toast, keep queue timing and visibility in the toast provider, then use Motion for the surface response. For Slider and Switch, let the component package own state and let Motion settle the moving part instead of deriving animation from raw pointer or toggle events.
@lattice-ui/motion Part of the stable direction toward v1.0.
What It Is For
Use @lattice-ui/motion when animation behavior needs to be shared across primitives instead of recreated inside each component.
It owns motion policy, presence transitions, response motion, feedback effects, interpolation, and the scheduler that applies those changes to Roblox instances.
The package is part of the Lattice UI foundation layer with core, focus, layer, style, and system.
TSX Example Preview
Preview the first lines below, or expand to inspect the full source file.
import {
createToggleResponseRecipe,
MotionProvider,
useResponseMotion,
} from "@lattice-ui/motion";
import React from "@rbxts/react";
... import {
createToggleResponseRecipe,
MotionProvider,
useResponseMotion,
} from "@lattice-ui/motion";
import React from "@rbxts/react";
export function MotionExample() {
const [reduced, setReduced] = React.useState(false);
return (
<MotionProvider mode={reduced ? "none" : "full"}>
<MotionPolicyDemo reduced={reduced} onReducedChange={setReduced} />
</MotionProvider>
);
}
function MotionPolicyDemo(props: {
reduced: boolean;
onReducedChange: (nextReduced: boolean) => void;
}) {
const [enabled, setEnabled] = React.useState(false);
const motionRef = useResponseMotion<TextButton>(
enabled,
{
active: {
BackgroundColor3: Color3.fromRGB(53, 104, 196),
Position: UDim2.fromOffset(136, 0),
},
inactive: {
BackgroundColor3: Color3.fromRGB(60, 76, 104),
Position: UDim2.fromOffset(0, 0),
},
},
createToggleResponseRecipe(),
);
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(440, 190)}>
<frame
BackgroundColor3={Color3.fromRGB(22, 28, 39)}
BorderSizePixel={0}
Size={UDim2.fromOffset(260, 44)}
>
<uicorner CornerRadius={new UDim(0, 8)} />
<textbutton
AutoButtonColor={false}
BorderSizePixel={0}
Size={UDim2.fromOffset(124, 44)}
Text={enabled ? "Enabled" : "Disabled"}
TextColor3={Color3.fromRGB(236, 240, 248)}
TextSize={14}
Event={{ Activated: () => setEnabled((value) => !value) }}
ref={motionRef}
>
<uicorner CornerRadius={new UDim(0, 8)} />
</textbutton>
</frame>
<textbutton
AutoButtonColor={false}
BackgroundColor3={Color3.fromRGB(34, 41, 54)}
BorderSizePixel={0}
Position={UDim2.fromOffset(0, 62)}
Size={UDim2.fromOffset(260, 36)}
Text={props.reduced ? "Reduced motion: on" : "Reduced motion: off"}
TextColor3={Color3.fromRGB(236, 240, 248)}
TextSize={14}
Event={{ Activated: () => props.onReducedChange(!props.reduced) }}
>
<uicorner CornerRadius={new UDim(0, 8)} />
</textbutton>
<textlabel
BackgroundTransparency={1}
Position={UDim2.fromOffset(0, 118)}
Size={UDim2.fromOffset(340, 44)}
Text="MotionProvider owns policy. The control owns state; motion only settles the visual response."
TextColor3={Color3.fromRGB(172, 181, 196)}
TextSize={13}
TextWrapped={true}
TextXAlignment={Enum.TextXAlignment.Left}
TextYAlignment={Enum.TextYAlignment.Top}
/>
</frame>
);
} Install
Global CLI command: lattice add motion
Monorepo local script
Use your package manager wrapper when running the local lattice command.
pnpm lattice add motionPublic Exports
-
MotionProvider -
useMotionPolicy -
createMotionTargetContract -
motionTargets -
useFeedbackEffect -
usePresenceMotionController -
usePresenceMotion -
useResponseMotion -
createPressFeedbackEffect -
createFocusAccentEffect -
createSurfaceRevealRecipe -
createCanvasGroupRevealRecipe -
createOverlayFadeRecipe -
createPopperEntranceRecipe -
createCanvasGroupPopperEntranceRecipe -
createIndicatorRevealRecipe -
createIndicatorSettleRecipe -
createSliderThumbResponseRecipe -
createToggleResponseRecipe -
createSelectionResponseRecipe -
createFieldResponseRecipe -
createProgressResponseRecipe -
createToastResponseRecipe -
validateMotionProperty -
readMotionProperty -
writeMotionProperty -
applyMotionProperties -
areMotionValuesEqual -
canInterpolateMotionValue -
interpolateMotionValue -
isMotionValueSettled -
createPlacementOffset
State Model
MotionProviderowns the app-level motion policy. Use it when a screen needs full motion or reduced motion throughmode="none".- Presence motion coordinates mounted, visible, exiting, and exited phases so overlays and indicators can finish exit work before disappearing.
- Response motion settles a control toward active or inactive state, such as selected menu items, slider thumbs, switch thumbs, and progress ranges.
- Feedback motion applies short-lived accent and recovery effects for press, focus, or interaction confirmation.
- Motion target contracts define whether motion owns appearance, an offset wrapper, a size wrapper, layout, or an explicit custom property set.
Key API
MotionProvider
Set app or subtree policy with mode="full" or mode="none" so reduced-motion behavior is centralized instead of scattered through components.
usePresenceMotionController
Use this for enter and exit phases when content must stay mounted until motion completes.
useResponseMotion
Use this when a persistent control needs to settle between active and inactive visual states.
useFeedbackEffect
Use this for transient press or focus effects that should accent and recover without creating local tween code.
motionTargets
Declare which properties motion owns so layout, placement, and visual styling do not fight each other.
Composition Patterns
Presence for mounted surfaces
Use presence motion for Dialog, Popover, Select, Tooltip, Menu, Combobox, and indicator content that needs exit transitions.
Response for controls
Use response motion for Slider, Switch, Progress, selected items, and other controls that move or settle while staying mounted.
Feedback for short interaction effects
Use feedback motion for press and focus accents when the effect should be brief and policy-aware.
Package-owned animation behavior
Extend motion through recipes, targets, and hooks in this package instead of adding one-off tweens inside primitives.
Cautions / Limits
motiondoes not replacelayer:layerowns portals, stacking, dismissal, and basic presence mounting;motionowns how animated state is applied while content is mounted.motiondoes not replacefocus: focus trapping, restoration, and keyboard flow still belong infocusand the consuming primitive.- Do not let motion own placement geometry unless you deliberately use a layout target; anchored overlays should let
poppercompute placement and let motion animate an isolated wrapper. - Loom preview is useful for iteration, but final timing, focus, and input behavior still need Roblox runtime verification.