# Shared Layout

> One element moving beats two swapping.

- Section: Motion
- URL: https://critly.vercel.app/shared-layout
- Published: 2026-07-16
- Source: https://github.com/kausthubh-coder/critly/blob/main/content/motion/shared-layout.mdx

When a tab highlight jumps from one tab to the next, your eye has to find it
again. When it slides, you never lose it. A **shared layout** animation
treats two elements in two places as one element that moves.

Click the tabs. Both rows follow the same state.

> **Interactive demo: Shared Layout.** Open https://critly.vercel.app/shared-layout to try it.

On the left the highlight disappears from one tab and appears under another.
On the right it travels, so it tells you where you are and where you came
from.

**If two elements are the same thing to the user, move one instead of
swapping two.**

In Motion you give both elements the same `layoutId`. When one unmounts and
the other mounts, Motion measures where the old one was and animates the new
one from there with a transform. This is the FLIP technique.

## From list to detail

The same idea carries further than tabs. Open a project, go back, then switch
to Shared and do it again.

> **Interactive demo: Shared Layout Detail.** Open https://critly.vercel.app/shared-layout to try it.

With a swap, the whole view is replaced and you have to re-read it to check
you landed in the right place. With a shared layout, the thumbnail and title
carry over and only the rest is new.

Keep it quick. Both demos use a spring with no bounce that settles in 300ms.

## Where it goes wrong

Shared layout scales elements to move them, and scale distorts corners and
shadows. Motion corrects `borderRadius` and `boxShadow`, but only when they
are set through `style`, not a class. Text inside a box that changes shape
can stretch, so the title above uses `layout="position"` to move without
scaling.

## Usage

**Motion**

```tsx
import { motion } from "motion/react";

{tabs.map((tab) => (
  <button key={tab} className="relative" onClick={() => setActive(tab)}>
    {tab === active && (
      <motion.span
        className="absolute inset-0 bg-white"
        layoutId="tab-indicator"
        style={{ borderRadius: 6 }}
        transition={{ type: "spring", duration: 0.3, bounce: 0 }}
      />
    )}
    <span className="relative">{tab}</span>
  </button>
))}
```

**CSS**

```css
/* Names must be unique, so name only the clicked thumbnail, in JS:
   thumb.style.viewTransitionName = "project-thumb";
   document.startViewTransition(() => openProject(id)); */

.detail-header {
  view-transition-name: project-thumb;
}

::view-transition-group(project-thumb) {
  animation-duration: 300ms;
  animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
```

Without React, the View Transition API does this natively. Same-document view
transitions work in Chrome, Edge, Safari and Firefox. They are not
interruptible, though: starting a new one skips the one in progress, so a
fast second click makes the morph snap to its end instead of turning around.

## Resources

- [How I Use Shared Layout Animations](https://jakub.kr/work/shared-layout-animations): Jakub Krehel on layoutId, with interactive examples of where morphs make sense.
- [Layout animations in Motion](https://motion.dev/docs/react-layout-animations): The official guide to layout and layoutId, including how to fix scale distortion.
- [FLIP Your Animations](https://aerotwist.com/blog/flip-your-animations/): Paul Lewis's original write-up of the First, Last, Invert, Play technique behind shared layout.
- [View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API): The native way to morph elements between two states of the page, without a library.
