Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/with-framer-motion-app-router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# with-framer-motion-app-router

Shared element transitions using Framer Motion with the Next.js App Router.

Key points:

- Use `app/template.tsx` as the client transition boundary with `AnimatePresence` and `LayoutGroup`.
- Mark motion-rendering components with `'use client'`.
- Use `initial={false}` on `AnimatePresence` to avoid SSR/client diffs.
- Prefer stable keys (e.g., `usePathname()`) for route transitions.
- Ensure shared elements use identical `layoutId` across routes.

Run locally:

```
pnpm install
pnpm dev
```

13 changes: 13 additions & 0 deletions examples/with-framer-motion-app-router/app/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Detail } from '../../components/Detail'

const photos: Record<string, { id: string; src: string; alt: string }> = {
'1': { id: '1', src: '/photos/1.jpg', alt: 'Photo 1' },
'2': { id: '2', src: '/photos/2.jpg', alt: 'Photo 2' },
'3': { id: '3', src: '/photos/3.jpg', alt: 'Photo 3' },
}

export default function Page({ params }: { params: { slug: string } }) {
const item = photos[params.slug] ?? photos['1']
return <Detail id={item.id} src={item.src} alt={item.alt} />
}

10 changes: 10 additions & 0 deletions examples/with-framer-motion-app-router/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react'

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body style={{ padding: 24 }}>{children}</body>
</html>
)
}

6 changes: 6 additions & 0 deletions examples/with-framer-motion-app-router/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Grid } from '../components/Grid'

export default function Page() {
return <Grid />
}

26 changes: 26 additions & 0 deletions examples/with-framer-motion-app-router/app/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'

import { ReactNode } from 'react'
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion'
import { usePathname } from 'next/navigation'

export default function Template({ children }: { children: ReactNode }) {
const pathname = usePathname()

return (
<LayoutGroup>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
{children}
</motion.div>
</AnimatePresence>
</LayoutGroup>
)
}

18 changes: 18 additions & 0 deletions examples/with-framer-motion-app-router/components/Detail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

import Link from 'next/link'
import { motion } from 'framer-motion'
import { SharedImage } from './SharedImage'

export function Detail({ id, src, alt }: { id: string; src: string; alt: string }) {
return (
<div style={{ display: 'grid', gap: 16 }}>
<Link href="/">← Back</Link>
<SharedImage id={id} src={src} alt={alt} width={1200} height={1200} />
<motion.p layout>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero.
</motion.p>
</div>
)
}

23 changes: 23 additions & 0 deletions examples/with-framer-motion-app-router/components/Grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'

import Link from 'next/link'
import { SharedImage } from './SharedImage'

const items = [
{ id: '1', src: '/photos/1.jpg', alt: 'Photo 1' },
{ id: '2', src: '/photos/2.jpg', alt: 'Photo 2' },
{ id: '3', src: '/photos/3.jpg', alt: 'Photo 3' },
]

export function Grid() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{items.map((item) => (
<Link key={item.id} href={`/${item.id}`} style={{ display: 'block' }}>
<SharedImage id={item.id} src={item.src} alt={item.alt} width={600} height={600} />
</Link>
))}
</div>
)
}

26 changes: 26 additions & 0 deletions examples/with-framer-motion-app-router/components/SharedImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'

import { motion } from 'framer-motion'
import Image from 'next/image'

export function SharedImage({ id, src, alt, width, height }: {
id: string
src: string
alt: string
width: number
height: number
}) {
return (
<motion.div layoutId={`photo-${id}`} style={{ borderRadius: 12, overflow: 'hidden' }}>
<Image
src={src}
alt={alt}
width={width}
height={height}
priority
sizes="(max-width: 768px) 100vw, 33vw"
/>
</motion.div>
)
}

7 changes: 7 additions & 0 deletions examples/with-framer-motion-app-router/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}

module.exports = nextConfig

20 changes: 20 additions & 0 deletions examples/with-framer-motion-app-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "with-framer-motion-app-router",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"framer-motion": "^11.0.0",
"next": "canary",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"engines": {
"node": ">=18.18.0"
}
}

Loading