Motion의 Layout animation은 어떠한 CSS property도 transitionable 하게 만들 수 있습니다.
예를 들어
flex-direction이나grid-template-columns같은 경우transition을 적용할 수 없지만motion의layout을 사용하면transition을 적용할 수 있습니다.
ayout prop은 justify-content, flex-start, flex-end등 애니메이션 적용이 불가능한 css value를 애니메이션이 가능하도록 만들어줍니다.
import {motion} from 'motion/react'
<motion.div
layout
style={{ justifyContent: isOn ? "flex-start" : "flex-end" }}
/>
사용법
어떠한 layout change도 리엑트를 리렌더링 시키기만 한다면 애니메이션을 적용할 수 있습니다.
css 변경 사항은 animate prpo이 아닌 style 속성을 통해 즉시 적용되어야 한다. 레이아웃이 애니메이션을 처리하기 때문입니다.
import {motion} from 'motion/react'
<motion.div layout style={{ width: isOpen ? "80vw" : 0 }} />
layoutId
layoutId 속성을 사용하면 두 요소를 일치시키고 그 사이를 애니메이션으로 연결할 수 있습니다.
새로운 컴포넌트가 추가될 때 해당 컴포넌트의 layoutId 속성이 기존 컴포넌트의 layoutId 속성과 일치하면, 해당 컴포넌트는 기존 컴포넌트에서 자동으로 애니메이션을 통해 사라집니다.
기존 element가 여전히 장착되어 있을 때 새로운 element가 들어오면, 기존 구성 요소에서 새로운 element로 자동으로 fade 됩니다.
import { AnimatePresence } from "motion/react"
import * as motion from "motion/react-client"
import { useState } from "react"
export default function SharedLayoutAnimation() {
const [selectedTab, setSelectedTab] = useState(tabs[0])
return (
<div style={container}>
<nav style={nav}>
<ul style={tabsContainer}>
{tabs.map((item) => (
<motion.li
key={item.label}
initial={false}
animate={{
backgroundColor:
item === selectedTab ? "#eee" : "#eee0",
}}
style={tab}
onClick={() => setSelectedTab(item)}
>
{`${item.icon} ${item.label}``}
{item === selectedTab ? (
<motion.div
style={underline}
layoutId="underline"
id="underline"
/>
) : null}
</motion.li>
))}
</ul>
</nav>
<main style={iconContainer}>
<AnimatePresence mode="wait">
<motion.div
key={selectedTab ? selectedTab.label : "empty"}
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{ duration: 0.2 }}
style={icon}
>
{selectedTab ? selectedTab.icon : "😋"}
</motion.div>
</AnimatePresence>
</main>
</div>
)
}
const container: React.CSSProperties = {
width: 480,
height: "60vh",
maxHeight: 360,
borderRadius: 10,
background: "white",
overflow: "hidden",
boxShadow:
"0 1px 1px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075)",
display: "flex",
flexDirection: "column",
}
const nav: React.CSSProperties = {
background: "#fdfdfd",
padding: "5px 5px 0",
borderRadius: "10px",
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
borderBottom: "1px solid #eeeeee",
height: 44,
}
const tabsStyles: React.CSSProperties = {
listStyle: "none",
padding: 0,
margin: 0,
fontWeight: 500,
fontSize: 14,
}
const tabsContainer: React.CSSProperties = {
...tabsStyles,
display: "flex",
width: "100%",
}
const tab: React.CSSProperties = {
...tabsStyles,
borderRadius: 5,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: "100%",
padding: "10px 15px",
position: "relative",
background: "white",
cursor: "pointer",
height: 24,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flex: 1,
minWidth: 0,
userSelect: "none",
color: "#0f1115",
}
const underline: React.CSSProperties = {
position: "absolute",
bottom: -2,
left: 0,
right: 0,
height: 2,
background: "var(--accent)",
}
const iconContainer: React.CSSProperties = {
display: "flex",
justifyContent: "center",
alignItems: "center",
flex: 1,
}
const icon: React.CSSProperties = {
fontSize: 128,
}
/**
* ============== Data ================
*/
const allIngredients = [
{ icon: "🍅", label: "Tomato" },
{ icon: "🥬", label: "Lettuce" },
{ icon: "🧀", label: "Cheese" },
{ icon: "🥕", label: "Carrot" },
{ icon: "🍌", label: "Banana" },
{ icon: "🫐", label: "Blueberries" },
{ icon: "🥂", label: "Champers?" },
]
const [tomato, lettuce, cheese] = allIngredients
const tabs = [tomato, lettuce, cheese]
Animation Presence
element를 애니메이션으로 원래 레이아웃으로 되돌릴 때, AnimatePresence를 사용하면 해당 요소가 DOM에 남아 있도록 하여 exit 애니메이션이 완료될 때까지 유지할 수 있습니다.
<AnimatePresence>
{isOpen && <motion.div layoutId="modal" />}
</AnimatePresence>
Customization
Layout 애니메이션은 transition prop을 사용하여 커스텀할 수 있습니다.
<motion.div layout transition={{ duration: 0.3 }} />
Layout 애니메이션에만 transition 적용
Layout 애니메이션에만 transition을 적용하려면 layout transition만 정의하면 됩니다.
<motion.div
layout
animate={{ opacity: 0.5 }}
transition={{
default: { ease: "linear" },
layout: { duration: 0.3 }
}}
/>
layoutId을 사용한 요소의 애니메이션 customazation
layoutId를 사용한 애니메이션은 애니메이션을 적용할 요소에게 정의된 transition이 적용됩니다.
<>
<motion.button
layoutId="modal"
onClick={() => setIsOpen(true)}
// This transition will be used when the modal closes
transition={{ type: "spring" }}
>
Open
</motion.button>
<AnimatePresence>
{isOn && (
<motion.dialog
layoutId="modal"
// This transition will be used when the modal opens
transition={{ duration: 0.3 }}
/>
)}
</AnimatePresence>
</>
스크롤 요소에 레이아웃 애니메이션 적용
스크롤 요소에 레이아웃 애니메이션을 적용하려면 layoutScroll prop을 사용합니다.
<motion.div layoutScroll style={{ overflow: "scroll" }} />
position:fixed요소에 레이아웃 애니메이션 적용
position:fixed요소에 레이아웃 애니메이션을 적용하려면 layoutRoot prop을 사용합니다.
<motion.div layoutRoot style={{ position: "fixed" }} />
Group Layout 애니메이션
레이아웃 애니메이션은 컴포넌트가 리렌더링 되고 layout 전환될 때 trigger 됩니다.
하지만 두 개 이상의 컴포넌트가 동시에 재렌더링되지 않지만 서로의 레이아웃에 영향을 미치는 경우 어떻게 될까요?
function Accordion() {
const [isOpen, setOpen] = useState(false)
return (
<motion.div
layout
style={{ height: isOpen ? "100px" : "500px" }}
onClick={() => setOpen(!isOpen)}
/>
)
}
function List() {
return (
<>
<Accordion />
<Accordion />
</>
)
}
Accordion 컴포넌트가 리렌더링되면 성능적인 고려 때문에 다른 Accordion 컴포넌트는 layout이 바뀌었다는 것을 감지하지 못합니다.
이런 상황에서 LayoutGroup컴포넌트를 사용하면 layout 변화를 동기화 할 수 있습니다.
import { LayoutGroup } from "motion/react"
function List() {
return (
<LayoutGroup>
<Accordion />
<Accordion />
</LayoutGroup>
)
}
그룹화된 motion element 중 어느 하나에서 레이아웃 변경이 감지되면, 해당 변경 사항은 모든 구성 요소에 걸쳐 레이아웃 애니메이션이 트리거 됩니다.
Scale Correction
transition을 사용하여 레이아웃을 애니메이션 화할 때 자식 element가 왜곡될 수 있습니다. 이를 수정하려면 해당 요소의 첫 번째 자식 element에도 레이아웃 속성을 적용해야 합니다.
import React from "react";
import { useState } from "react";
import { motion } from "framer-motion";
import "./styles.css";
export default function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<motion.div
layout
data-isOpen={isOpen}
initial={{ borderRadius: 50 }}
className="parent"
onClick={() => setIsOpen(!isOpen)}>
<motion.div layout transition={{ duration: 1 }} className="child" />
</motion.div>
);
}'React > Interactive' 카테고리의 다른 글
| [Framer motion] 기본 사용법 (0) | 2025.07.13 |
|---|