Responsive Animations with Framer Motion
(Updated Aug 15, 2023)
Framer Motion makes it super easy to create great looking animations, but what if you want to have different animations for different screen sizes? With CSS animations you can just use a media query but did you know that by utilising the Window.matchMedia API or using CSS variables we can write responsive animations for javascript animation libraries like Framer Motion & React Spring?
Using media queries in javascript
The easiest way to make a responsive animation for Framer Motion is using the window.matchMedia API in React with a custom hook. A lot of UI frameworks like Material UI and Chakra UI already exposes such a hook, but here's what it could look like if you would write your own:
export function useMediaQuery(query) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => {
setMatches(media.matches);
};
if (typeof media.addEventListener === "function") {
media.addEventListener("change", listener);
} else {
media.addListener(listener);
}
return () => {
if (typeof media.removeEventListener === "function") {
media.removeEventListener("change", listener);
} else {
media.removeListener(listenerList);
}
};
}, [matches, query]);
return matches;
}
Not all media query hooks works optimally with server side rendering, by only updating the value on the client. If this is important to you, consider using the CSS variables option described below.
It takes a media query string (the same way you would write it in css) and returns true
if the query matches the current screen. If the screen is resized the value will update. Use it in code like this:
const isSmall = useMediaQuery("(min-width: 480px)");
A good idea is to set up custom hooks matching the media queries you already use in your css so you don't have to remember the pixel values (was it 479 or 480?):
export const useIsSmall = () => useMediaQuery('(min-width: 480px)');
export const useIsMedium = () => useMediaQuery('(min-width: 768px)');
/* etc.. /*
Responsive Animation with Variants
Now that we have our hooks setup lets put it all together by making an example using variants to conditionally change based on the media query.
import { motion } from 'framer-motion'
import { useIsSmall } from 'src/hooks/utils'
const Component = () => {
const isSmall = useIsSmall() /* or useMediaQuery('(min-width: 480px)'); */
const variants = isSmall
? {
animate: {
opacity: 1,
scale: 1,
y: 0,
},
exit: {
opacity: 1,
scale: 1,
y: 500,
},
}
: {
animate: {
opacity: 1,
scale: 1,
y: 0,
},
exit: {
opacity: 0,
scale: 0.9,
y: -10,
},
};
return (
<motion.div initial="exit" animate="animate" exit="exit">Animated</div>
);
}
You can also use the variable inline <motion.div animate={isSmall ? { y: 500} : { y: 1000}} />
but I find that using it together with variants is the cleanest way most of the time.
Responsive Framer Motion with CSS Variables
Another way to make responsive animations with Framer Motion is to use css variables, as shown using Tailwind CSS in this excellent video by Sam Selikoff. This is slightly more complicated than using a media query hook but has the advantage of working optimally with server side rendering.
The trick is to use CSS variables as the values of your variants and then update the variables across breakpoints using a media query. Here's a refactor of the previous example:
const Component = () => {
const variants = {
animate: {
opacity: var("--opacity-animate"),
scale: var("--scale-animate"),
y: var("--y-animate"),
},
exit: {
opacity: var("--opacity-exit"),
scale: var("--scale-exit"),
y: var("--y-exit"),
},
}
return (
<motion.div
initial="exit"
animate="animate"
exit="exit"
className="component"
>
Animated
</div>
);
}
And then in your css override the variables with media queries. Note that you only need to specify the values that change in the breakpoint, the rest will be inherited from the inital values.
.component {
--opacity-animate: 1;
--scale-animate: 1;
--y-animate: "0px";
--opacity-exit: 0;
--scale-exit: 0.9;
--y-exit: "-10px";
@media (min-width: 480px) {
--opacity-exit: 1;
--scale-exit: 1;
--y-exit: "500px";
}
}
Check out Sam's video above for a more in depth explanation of this technique and how to use it with Tailwind CSS.