---
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 && (
)}
);
}
```
### 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
// 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
```