Skip to main content
Version: next

LayoutGroup

animate.* components support automatic layout-change animations via three props: layout, layoutId, and layoutOptions. LayoutGroup scopes layoutId transitions to a subtree.

layout

Set layout to animate position/size changes caused by reordering, resizing, or sibling insertion/removal: a FLIP-style transform animation, composed with whatever transform the element already has from style/ animate/hover/etc.

<animate.div layout style={{ width: expanded ? 300 : 100 }} />

layoutId: shared-element transitions

Give two different elements the same layoutId. When the one carrying it unmounts (or a new one mounts) with a matching layoutId already active, the new element automatically plays a FLIP transition from the previous element's last known position/size to its own (the classic "morph between elements" pattern used for tab indicators, expanding cards, and shared images between a list and a detail view).

{selectedTab === 'a' && <animate.div layoutId="indicator" />}
{selectedTab === 'b' && <animate.div layoutId="indicator" />}

layoutOptions sets the transition (spring or timing, options-only, with no target, since a FLIP transform always settles at identity):

<animate.div layoutId="card" layoutOptions={withSpring({ stiffness: 400, damping: 32 })} />

LayoutGroup

layoutIds are tracked in a single global registry by default, so two unrelated parts of your app using the same layoutId string would cross-transition with each other. Wrap a subtree in LayoutGroup to scope its layoutIds to that subtree instead:

import { LayoutGroup, animate } from 'react-ui-animate';

function TabsA() {
return (
<LayoutGroup>
{tab === 'a1' && <animate.div layoutId="indicator" />}
{tab === 'a2' && <animate.div layoutId="indicator" />}
</LayoutGroup>
);
}

function TabsB() {
return (
<LayoutGroup>
{tab === 'b1' && <animate.div layoutId="indicator" />}
{tab === 'b2' && <animate.div layoutId="indicator" />}
</LayoutGroup>
);
}

TabsA and TabsB both use layoutId="indicator", but each is scoped to its own LayoutGroup, so they won't cross-animate.

Elements outside any LayoutGroup keep using the global registry, so existing layoutId usage without a LayoutGroup is unaffected.