[Framer motion] Layout

2025. 7. 13. 18:39·React/Interactive

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
'React/Interactive' 카테고리의 다른 글
  • [Framer motion] 기본 사용법
월월월월
월월월월
  • 월월월월
    Bobostown
    월월월월
  • 전체
    오늘
    어제
    • 분류 전체보기 (43)
      • 개발 (4)
      • 사이드 프로젝트 (1)
        • interview-lab (1)
        • Loft (0)
      • Claude (1)
      • React (5)
        • React Router (1)
        • Interactive (2)
      • Javascript (1)
      • node.js (3)
      • npm (3)
      • Nest.js (0)
      • Web (5)
        • Web API (4)
      • TDD (2)
        • Jest (0)
      • TroubleShooting (1)
      • Rust (1)
      • Bash (1)
      • 보안 (1)
      • 일상 (4)
      • 여행 (5)
      • 우아한 테크코스 8기 프리코스 (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    오픈미션
    VITE
    react
    private package
    nofitication
    Media Capture and Streams API
    package.json
    framer motion
    runzipper
    오실리에이터
    Web API
    devfest 2025
    LZ77
    motion
    webbase line
    npm
    보홀
    peer dependency
    json-schema
    node.js
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
월월월월
[Framer motion] Layout
상단으로

티스토리툴바