Spatial consistency
Also called spatial continuity
Moving elements in ways that match where things live in the interface, so people can tell where an item went and how to find it again.
import { useState } from 'react';
import { useAnimate, useReducedMotion } from 'motion/react';
type Email = { id: string; from: string; subject: string };
// An emptied slot closes completely, padding and border included.
const CLOSED = { height: 0, paddingTop: 0, paddingBottom: 0, borderWidth: 0 };
export function Inbox({ initial }: { initial: Email[] }) {
const [emails, setEmails] = useState(initial);
const [archived, setArchived] = useState(0);
const [archiveButton, animate] = useAnimate<HTMLButtonElement>();
const reduceMotion = useReducedMotion();
// The row flies into the Archive button, so people see where it went.
async function archive(id: string, button: HTMLElement) {
const row = button.closest('li')!;
const from = row.getBoundingClientRect();
const to = archiveButton.current.getBoundingClientRect();
const x = to.x + to.width / 2 - (from.x + from.width / 2);
const y = to.y + to.height / 2 - (from.y + from.height / 2);
await animate(
row,
reduceMotion ? { opacity: 0 } : { x, y, scale: 0.2, opacity: 0 },
{ duration: reduceMotion ? 0.15 : 0.45, ease: 'easeInOut' }
);
setArchived((count) => count + 1);
animate(archiveButton.current, { scale: [1, 1.1, 1] }, { duration: 0.3 });
// Close the empty slot so the rows below slide up.
await animate(row, CLOSED, { duration: 0.2 });
setEmails((list) => list.filter((email) => email.id !== id));
}
return (
<section className='w-96 rounded-2xl border bg-background text-sm'>
<header className='flex justify-end border-b p-2'>
<button ref={archiveButton} type='button' className='px-3 py-1.5'>
Archive ({archived})
</button>
</header>
<ul>
{emails.map((email) => (
<li
key={email.id}
className='flex items-center gap-3 overflow-hidden border-b px-4 py-3 last:border-b-0'
>
<span className='w-24 shrink-0 font-medium'>{email.from}</span>
<span className='flex-1 truncate text-muted-foreground'>
{email.subject}
</span>
<button
type='button'
onClick={(event) => archive(email.id, event.currentTarget)}
className='rounded-md px-2 py-1 text-xs hover:bg-muted'
>
Archive
</button>
</li>
))}
</ul>
</section>
);
}When the user archives an email, shrink the row to 20% and move it into the Archive button in the toolbar over 450ms with an ease-in-out curve, then bump the button to 1.1 scale and increase its count. Slide the remaining rows up to close the gap. When prefers-reduced-motion is set, fade the row out over 150ms instead.
Paste into Layerwise or any AI coding tool.
What is Spatial consistency?
Spatial consistency means the interface behaves like a real place. Archive an email and it shrinks into the Archive button; a panel that lives on the right slides in from the right and leaves the same way; a photo opened from a grid grows out of its thumbnail. Each motion answers the question "where did that go?"
When items vanish or appear from nowhere, people lose track and have to rebuild their mental map of the app. Motion with a consistent direction and destination teaches the layout without any words.
Keep directions consistent across the app: going forward moves one way and going back reverses it. An item flying to a destination usually takes 300 to 500ms. When prefers-reduced-motion is set, replace the travel with a quick fade.
Often confused with
- Shared element transition
- A shared element transition morphs one element into its counterpart on the next screen. Spatial consistency is the broader rule that all motion should match where things live.
Related terms
All termsShared element transition
A transition where an element from one view, like a thumbnail, travels and grows into its place in the next view, linking the two screens.
Direction-aware transition
A transition that slides new content in from the side you are heading toward, so going forward and going back move in opposite directions.
Continuity transition
A transition where one element keeps its identity as it changes role, like an “Add to cart” button that becomes a progress bar, then a checkmark.
Purposeful animation
Animation used only where it helps people notice a change, follow where something went, or understand what just happened.
Build it with Layerwise
Describe your app, watch it run in a live preview, and publish it with a database, storage, and sign-in built in.