install
npm install framer-motion
특이사항
<div></div>
<motion.div></motion.div>
<div />
등과 같이 HTML 태그는 <motion.div />
로 사용한다.
const Box = styled(motion.div)`
height: 200px;
background-color: rgba(255, 255, 255, 1);
border-radius: 40px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;
styled-components로 사용하기 위해선 위와 같이 styled(motion.div)
Variants
const boxVariants = {
start: { opacity: 0, scale: 0.5 },
end: { scale: 1, opacity: 1, transition: { type: "spring" } },
};
위와 같은 variants를 생성해준 뒤,
<Box variants={boxVariants} initial="start" animate="end">
<Circle />
<Circle />
<Circle />
<Circle />
</Box>
<Box></Box>
컴포넌트에 주면, 자식 Circle
컴포넌트들에도 시작으로 start property 값, 끝으로 end property 값들을 주는 것과 다름 없다.
AnimationPresence
prop으로 initial
animate
exit
<AnimatePresence>
{id ? (
<Overlay
onClick={() => setId(null)}
initial={{ backgroundColor: "rgba(0, 0, 0, 0)" }}
animate={{ backgroundColor: "rgba(0, 0, 0, 0.5)" }}
exit={{ backgroundColor: "rgba(0, 0, 0, 0)" }}
>
<Box layoutId={id} style={{ width: 400, height: 200 }} />
</Overlay>
) : null}
</AnimatePresence>
같은 layoutId
를 가진 컴포넌트 사이의 연결을 도우며 "애니메이션"으로 보여준다.
연습 코드
import React, { useState } from "react";
import styled from "styled-components";
import { motion, AnimatePresence, useAnimation } from "framer-motion";
const Wrapper = styled.div`
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
`;
const Grid = styled.div`
display: grid;
width: 50vw;
gap: 10px;
grid-template-columns: repeat(2, 1fr);
&:button {
display: flex;
}
`;
const Box = styled(motion.div)`
display: flex;
align-items: center;
justify-content: center;
height: 200px;
background-color: green;
border-radius: 5px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
transform-origin: ${(props) =>
props.hov === "hov"
? "bottom right"
: props.hov === "no"
? "top left"
: null};
`;
const Overlay = styled(motion.div)`
width: 100%;
height: 100%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
`;
const Circle = styled(motion.span)`
width: 5rem;
height: 5rem;
background: black;
position: absolute;
border-radius: 50%;
margin: 0 auto;
`;
const BoxVariants = {
hover: { scale: 1.3, transition: { type: "tween" } },
init: { scale: 1 }
};
const ButtonVariants = {
active: {
color: "red",
scale: 1.3,
transition: { type: "tween", duration: 0.5 }
},
disabled: { scale: 1 }
};
const Button = styled(motion.button)`
height: 30px;
width: 80px;
border: 1px solid gray;
background-color: transparent;
border-radius: 5px;
display: flex-end;
margin: 30px;
color: blue;
cursor: pointer;
`;
const Column = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 30px;
`;
function App() {
const [id, setId] = useState(null);
const [show, setIsShown] = useState(true);
const [active, setIsActive] = useState(false);
const circleClick = () => {
setIsActive(!active);
setIsShown(!show);
};
return (
<>
<Wrapper>
<Column>
<Grid>
{["1", "2", "3", "4"].map((n) =>
n === "1" || n === "4" ? (
<Box
hov={n === "1" ? "hov" : "no"}
variants={BoxVariants}
initial="init"
whileHover="hover"
onClick={() => setId(n)}
key={n}
layoutId={n}
/>
) : (
<Box onClick={() => setId(n)} key={n} layoutId={n}>
{show && n === "2" ? (
<Circle
layoutId="circle"
animate={{ transition: { duration: 1 } }}
/>
) : null}
{!show && n === "3" ? (
<Circle
layoutId="circle"
animate={{ transition: { duration: 1 } }}
/>
) : null}
</Box>
)
)}
</Grid>
<Button
variants={ButtonVariants}
animate={active ? "active" : "disabled"}
onClick={circleClick}
>
SWITCH
</Button>
</Column>
<AnimatePresence>
{id ? (
<Overlay
onClick={() => setId(null)}
initial={{ backgroundColor: "rgba(0, 0, 0, 0)" }}
animate={{ backgroundColor: "rgba(0, 0, 0, 0.5)" }}
exit={{ backgroundColor: "rgba(0, 0, 0, 0)" }}
>
<Box layoutId={id} style={{ width: 300, height: 200 }} />
</Overlay>
) : null}
</AnimatePresence>
</Wrapper>
</>
);
}
export default App;