--- name: animations-transitions description: MUI transition components (Fade, Grow, Slide, Collapse, Zoom), custom transitions, TransitionGroup, and Framer Motion integration triggers: - animation - transition - Fade - Grow - Slide - Collapse - Zoom - TransitionGroup - Framer Motion allowed-tools: - Read - Glob - Grep - Write - Edit globs: - "*.tsx" - "*.jsx" --- # MUI Animations & Transitions ## 1. Built-in Transition Components MUI provides five transition components built on top of `react-transition-group`. Each wraps a single child element and controls its enter/exit animation based on the `in` prop. ### Fade Opacity transition from transparent to opaque. ```tsx import { Fade, Box, Button } from '@mui/material'; import { useState } from 'react'; function FadeExample() { const [visible, setVisible] = useState(false); return ( <> Fading content ); } ``` **Key props:** | Prop | Type | Default | Description | |------|------|---------|-------------| | `in` | `boolean` | `false` | Controls visibility | | `timeout` | `number \| { enter, exit }` | `300` | Duration in ms | | `appear` | `boolean` | `true` | Animate on initial mount when `in` is true | | `unmountOnExit` | `boolean` | `false` | Remove DOM node when exited | | `mountOnEnter` | `boolean` | `false` | Defer first mount until `in` is true | ### Grow Combined scale and opacity transition. The element grows from the center (or a custom origin) while fading in. ```tsx import { Grow, Paper } from '@mui/material'; function GrowExample({ visible }: { visible: boolean }) { return ( Growing content ); } ``` **Custom transform origin:** ```tsx ``` ### Slide Directional slide transition. The element slides in from an edge of the screen or a container. ```tsx import { Slide, Paper } from '@mui/material'; import { useRef } from 'react'; function SlideExample({ visible }: { visible: boolean }) { const containerRef = useRef(null); return ( Slides up into view ); } ``` **`direction` values:** `'up'` | `'down'` | `'left'` | `'right'` When `container` is provided, the element slides relative to that container instead of the viewport. ### Collapse Height (or width) animation that expands/collapses content. ```tsx import { Collapse, List, ListItem, ListItemText, Button, Box } from '@mui/material'; function CollapseExample() { const [expanded, setExpanded] = useState(false); return ( ); } ``` **Horizontal collapse:** ```tsx ``` | Prop | Type | Default | Description | |------|------|---------|-------------| | `collapsedSize` | `number \| string` | `'0px'` | Minimum size when collapsed (e.g., `40` to keep a peek visible) | | `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Direction of collapse | | `timeout` | `number \| 'auto'` | `duration.standard` | `'auto'` calculates duration from height | ### Zoom Scale transition from the center of the child element. ```tsx import { Zoom, Fab, AddIcon } from '@mui/material'; function ZoomFab({ visible }: { visible: boolean }) { return ( ); } ``` --- ## 2. Usage Patterns ### Basic Pattern ```tsx Content ``` ### Ref Forwarding for Custom Components MUI transition components pass a `ref` to their child. If the child is a custom component, it must forward the ref: ```tsx import { forwardRef } from 'react'; import { Fade, FadeProps } from '@mui/material'; // Custom component that forwards ref const CustomCard = forwardRef( function CustomCard({ title, ...props }, ref) { return (

{title}

); } ); // Usage inside a transition function AnimatedCard({ visible }: { visible: boolean }) { return ( ); } ``` Without `forwardRef`, the transition will not work and React will warn about refs on function components. ### Conditional Rendering vs Visibility Toggle **Visibility toggle** (keeps DOM node, hides with CSS): ```tsx Always in DOM, opacity changes ``` **Conditional rendering** (removes DOM node on exit): ```tsx Removed from DOM when hidden ``` Use `unmountOnExit` when: - The hidden content is expensive (heavy components, iframes) - You need to reset component state on re-entry - You want to reduce DOM size for accessibility screen readers Use visibility toggle when: - You need instant re-show without remount cost - The component maintains scroll position or form state --- ## 3. TransitionGroup -- Animating Lists `TransitionGroup` from `react-transition-group` works with MUI transitions to animate items being added or removed from a list. ```tsx import { TransitionGroup } from 'react-transition-group'; import { Collapse, List, ListItem, ListItemText, IconButton, Button, Box, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import { useState } from 'react'; interface Item { id: number; name: string; } function AnimatedList() { const [items, setItems] = useState([ { id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }, { id: 3, name: 'Cherry' }, ]); const addItem = () => { const id = Date.now(); setItems((prev) => [...prev, { id, name: `Item ${id}` }]); }; const removeItem = (id: number) => { setItems((prev) => prev.filter((item) => item.id !== id)); }; return ( {items.map((item) => ( removeItem(item.id)}> } > ))} ); } ``` **Using Fade instead of Collapse for list items:** ```tsx {items.map((item) => ( ))} ``` **Important:** Each child of `TransitionGroup` must have a unique `key`. The transition component (Collapse, Fade, etc.) should be the direct child of TransitionGroup, wrapping the actual content. --- ## 4. Custom Transitions Create reusable transition components using the `Transition` component from `react-transition-group`. ### Using MUI's `styled` + Transition ```tsx import { Transition, TransitionStatus } from 'react-transition-group'; import { forwardRef, useRef } from 'react'; import { Box, BoxProps } from '@mui/material'; interface SlideRotateProps extends Omit { in: boolean; timeout?: number; children: React.ReactNode; } const SlideRotate = forwardRef( function SlideRotate({ in: inProp, timeout = 400, children, ...boxProps }, ref) { const nodeRef = useRef(null); const styles: Record = { entering: { opacity: 1, transform: 'translateX(0) rotate(0deg)' }, entered: { opacity: 1, transform: 'translateX(0) rotate(0deg)' }, exiting: { opacity: 0, transform: 'translateX(-100%) rotate(-10deg)' }, exited: { opacity: 0, transform: 'translateX(-100%) rotate(-10deg)' }, unmounted: {}, }; return ( {(state) => ( {children} )} ); } ); // Usage function Demo() { const [show, setShow] = useState(true); return ( Custom transition! ); } ``` ### Wrapping MUI Transitions for Reuse ```tsx import { Slide, SlideProps } from '@mui/material'; // A preset "slide from right" transition component function SlideFromRight(props: Omit) { return ; } // A combined Fade + Slide transition function FadeSlideUp({ in: inProp, children, timeout = 400, }: { in: boolean; children: React.ReactElement; timeout?: number; }) { return (
{children}
); } ``` --- ## 5. Theme Transitions MUI's theme provides a `transitions` object for consistent timing across the application. ### theme.transitions.create() Generates a CSS transition string from property names and options: ```tsx import { Box, useTheme } from '@mui/material'; function ThemedTransition() { const theme = useTheme(); return ( ); } ``` ### Using the Theme Callback in `sx` ```tsx theme.transitions.create(['background-color', 'box-shadow'], { duration: theme.transitions.duration.short, }), '&:hover': { bgcolor: 'action.hover', boxShadow: 4, }, }} /> ``` ### Available Duration Constants | Constant | Value | Use case | |----------|-------|----------| | `shortest` | 150ms | Small UI feedback (ripple) | | `shorter` | 200ms | Quick toggles | | `short` | 250ms | Standard interactions | | `standard` | 300ms | Default for most transitions | | `complex` | 375ms | Multi-property changes | | `enteringScreen` | 225ms | Elements entering viewport | | `leavingScreen` | 195ms | Elements leaving viewport | ### Available Easing Constants | Constant | Value | Use case | |----------|-------|----------| | `easeInOut` | `cubic-bezier(0.4, 0, 0.2, 1)` | Standard transitions | | `easeOut` | `cubic-bezier(0.0, 0, 0.2, 1)` | Elements entering screen | | `easeIn` | `cubic-bezier(0.4, 0, 1, 1)` | Elements leaving screen | | `sharp` | `cubic-bezier(0.4, 0, 0.6, 1)` | Elements that may return | ### Customizing Theme Transitions ```tsx import { createTheme } from '@mui/material/styles'; const theme = createTheme({ transitions: { duration: { shortest: 100, shorter: 150, short: 200, standard: 250, complex: 300, enteringScreen: 200, leavingScreen: 150, }, easing: { easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)', easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)', easeIn: 'cubic-bezier(0.4, 0, 1, 1)', sharp: 'cubic-bezier(0.4, 0, 0.6, 1)', // Custom easing: bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', }, }, }); ``` --- ## 6. sx-based CSS Transitions and Keyframes ### Hover Transitions with sx ```tsx ``` ### Keyframe Animations with @keyframes ```tsx import { keyframes } from '@mui/system'; import { Box } from '@mui/material'; const pulse = keyframes` 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(25, 118, 210, 0.4); } 70% { transform: scale(1.05); box-shadow: 0 0 0 10px rgba(25, 118, 210, 0); } 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(25, 118, 210, 0); } `; const spin = keyframes` from { transform: rotate(0deg); } to { transform: rotate(360deg); } `; const shimmer = keyframes` 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } `; function KeyframeExamples() { return ( <> {/* Pulsing notification badge */} {/* Spinning loader */} {/* Shimmer loading placeholder */} `linear-gradient(90deg, ${theme.palette.grey[200]} 25%, ${theme.palette.grey[100]} 50%, ${theme.palette.grey[200]} 75%)`, backgroundSize: '200% 100%', animation: `${shimmer} 1.5s ease-in-out infinite`, }} /> ); } ``` ### Combining Keyframes with Theme ```tsx const fadeInUp = keyframes` from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } `; ``` ### Staggered Animations ```tsx function StaggeredList({ items }: { items: string[] }) { return ( {items.map((item, index) => ( ))} ); } ``` --- ## 7. Framer Motion Integration Framer Motion provides more advanced animation capabilities that work well alongside MUI components. ### Install ```bash npm install framer-motion ``` ### AnimatePresence with MUI Dialog ```tsx import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { AnimatePresence, motion } from 'framer-motion'; const MotionDialogContent = motion.create(DialogContent); function AnimatedDialog({ open, onClose, }: { open: boolean; onClose: () => void; }) { return ( {open && ( Animated Dialog Content appears with a slight delay after the dialog scales in. )} ); } ``` ### Layout Animations with MUI Components ```tsx import { motion, LayoutGroup } from 'framer-motion'; import { Card, CardContent, Typography, Grid, Box } from '@mui/material'; const MotionCard = motion.create(Card); function LayoutAnimationGrid({ items }: { items: Item[] }) { const [selected, setSelected] = useState(null); return ( {items.map((item) => ( setSelected(selected === item.id ? null : item.id) } sx={{ cursor: 'pointer' }} transition={{ duration: 0.4, ease: 'easeInOut' }} > {item.title} {selected === item.id && ( {item.description} )} ))} ); } ``` ### Animated List with AnimatePresence ```tsx import { AnimatePresence, motion } from 'framer-motion'; import { List, ListItem, ListItemText, IconButton } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; const MotionListItem = motion.create(ListItem); function MotionList({ items, onRemove }: { items: Item[]; onRemove: (id: string) => void }) { return ( {items.map((item) => ( onRemove(item.id)}> } > ))} ); } ``` ### Shared Layout Animations (Tabs) ```tsx import { motion } from 'framer-motion'; import { Tabs, Tab, Box } from '@mui/material'; function AnimatedTabs() { const [tab, setTab] = useState(0); const tabs = ['Overview', 'Details', 'Settings']; return ( {tabs.map((label, i) => ( setTab(i)} sx={{ px: 2, py: 1, cursor: 'pointer', position: 'relative', zIndex: 1, color: tab === i ? 'primary.contrastText' : 'text.primary', transition: 'color 0.3s', }} > {tab === i && ( )} {label} ))} ); } ``` --- ## 8. Component-Specific Transitions ### Dialog Enter/Exit Override the default Dialog transition: ```tsx import { Dialog, Slide } from '@mui/material'; import { TransitionProps } from '@mui/material/transitions'; import { forwardRef } from 'react'; const SlideUpTransition = forwardRef(function SlideUpTransition( props: TransitionProps & { children: React.ReactElement }, ref: React.Ref, ) { return ; }); // Full-screen dialog with slide-up entrance {/* ... */} ``` ### Drawer Slide Drawers use Slide internally. Customize via `SlideProps`: ```tsx ``` ### Snackbar Transitions ```tsx import { Snackbar, Slide, Grow, Fade } from '@mui/material'; // Slide from top function SlideDown(props: TransitionProps) { return ; } // Grow from the anchor ``` ### Menu and Popover Transitions ```tsx import { Menu, MenuItem, Fade, Zoom } from '@mui/material'; // Fade menu instead of default Grow Option 1 Option 2 // Zoom popover Popover content ``` ### Accordion Collapse Accordion uses Collapse internally. Customize via `TransitionProps` and `TransitionComponent`: ```tsx import { Accordion, AccordionSummary, AccordionDetails, Fade } from '@mui/material'; }> Section Title Lazy-mounted content that unmounts on collapse. ``` ### Skeleton Pulse Customization Override the default pulse animation of Skeleton: ```tsx import { Skeleton, keyframes } from '@mui/material'; const customPulse = keyframes` 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } `; // Wave animation variant // Disable animation ``` --- ## 9. Performance Tips ### Use GPU-Accelerated Properties Only animate `transform` and `opacity` for smooth 60fps animations. These properties do not trigger layout or paint: ```tsx // GOOD: GPU-accelerated sx={{ transition: 'transform 0.3s, opacity 0.3s', '&:hover': { transform: 'translateY(-4px) scale(1.02)', opacity: 0.9, }, }} // BAD: Triggers layout recalculation sx={{ transition: 'width 0.3s, height 0.3s, margin 0.3s', '&:hover': { width: 300, // layout thrash height: 200, // layout thrash marginTop: 10, // layout thrash }, }} ``` ### will-change Hint Tell the browser to prepare for upcoming animations: ```tsx theme.transitions.create(['transform', 'opacity'], { duration: theme.transitions.duration.standard, }), '&:hover': { transform: 'scale(1.05)', }, }} /> ``` **Important:** Only apply `will-change` to elements that will actually animate. Overuse wastes GPU memory. Remove it after the animation completes for one-shot animations: ```tsx function AnimateOnce({ children }: { children: React.ReactNode }) { const [animated, setAnimated] = useState(false); return ( setAnimated(true)} > {children} ); } ``` ### Avoid Layout Thrash Do not read layout properties (offsetHeight, getBoundingClientRect) in the middle of an animation frame. Batch reads before writes: ```tsx // BAD: read-write-read-write causes forced reflows elements.forEach((el) => { const height = el.offsetHeight; // read (forces layout) el.style.transform = `translateY(${height}px)`; // write }); // GOOD: batch all reads, then all writes const heights = elements.map((el) => el.offsetHeight); elements.forEach((el, i) => { el.style.transform = `translateY(${heights[i]}px)`; }); ``` ### Reduce Motion for Accessibility Respect the user's `prefers-reduced-motion` setting: ```tsx const prefersReducedMotion = keyframes`/* empty */`; ``` Or globally via the theme: ```tsx const theme = createTheme({ transitions: { // Check user preference create: (props, options) => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return 'none'; } return createTheme().transitions.create(props, options); }, }, }); ``` ### Transition Performance Checklist - Animate only `transform` and `opacity` whenever possible - Use `will-change` sparingly and only on elements about to animate - Prefer `unmountOnExit` on heavy content behind transitions - Use `timeout="auto"` on Collapse to get natural-feeling durations - Set `appear={false}` to skip initial mount animations when not needed - Use `requestAnimationFrame` for JavaScript-driven animations - Respect `prefers-reduced-motion` for accessibility compliance - Avoid animating `box-shadow` directly; use `::after` pseudo-element with opacity instead: ```tsx ```