Skip to main content
Radix Primitives are built with animation in mind. Components expose data attributes that make it easy to target different states, and the Presence component handles exit animations.

Data State Attributes

Most interactive Radix components provide data-state attributes that reflect their current state. You can use these to apply animations:
import * as Dialog from '@radix-ui/react-dialog';
import './styles.css';

function MyDialog() {
  return (
    <Dialog.Root>
      <Dialog.Trigger>Open</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="dialog-overlay" />
        <Dialog.Content className="dialog-content">
          <Dialog.Title>Animated Dialog</Dialog.Title>
          <Dialog.Close>Close</Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

CSS Transitions

Use CSS transitions with data attributes for simple animations:
.dialog-overlay {
  background-color: rgba(0, 0, 0, 0.5);
  position: fixed;
  inset: 0;
  transition: opacity 150ms ease-in-out;
}

.dialog-overlay[data-state='open'] {
  opacity: 1;
}

.dialog-overlay[data-state='closed'] {
  opacity: 0;
}

.dialog-content {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  border-radius: 8px;
  padding: 24px;
  transition: all 200ms ease-in-out;
}

.dialog-content[data-state='open'] {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.dialog-content[data-state='closed'] {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.95);
}
Components expose data-state with values like "open", "closed", "checked", "unchecked", etc.

CSS Keyframe Animations

Use CSS keyframes for more complex animations:
@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideUp {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-12px);
  }
}

.dropdown-content[data-state='open'] {
  animation: slideDown 200ms ease-out;
}

.dropdown-content[data-state='closed'] {
  animation: slideUp 150ms ease-in;
}

Example with Accordion

import * as Accordion from '@radix-ui/react-accordion';
import './accordion.css';

function AnimatedAccordion() {
  return (
    <Accordion.Root type="single" collapsible>
      <Accordion.Item value="item-1">
        <Accordion.Header>
          <Accordion.Trigger className="accordion-trigger">
            What is Radix?
          </Accordion.Trigger>
        </Accordion.Header>
        <Accordion.Content className="accordion-content">
          Radix is an open-source UI component library.
        </Accordion.Content>
      </Accordion.Item>
    </Accordion.Root>
  );
}
.accordion-content {
  overflow: hidden;
}

.accordion-content[data-state='open'] {
  animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
}

.accordion-content[data-state='closed'] {
  animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
}

@keyframes slideDown {
  from {
    height: 0;
  }
  to {
    height: var(--radix-accordion-content-height);
  }
}

@keyframes slideUp {
  from {
    height: var(--radix-accordion-content-height);
  }
  to {
    height: 0;
  }
}
Radix provides CSS variables like --radix-accordion-content-height and --radix-collapsible-content-width for animating dynamic dimensions.

Exit Animations

Radix uses the Presence component internally to handle exit animations. The component stays mounted while animating out, allowing CSS animations to complete.

Force Mount for Animation Control

For advanced animation control, use the forceMount prop:
import * as Dialog from '@radix-ui/react-dialog';

function ControlledDialog() {
  return (
    <Dialog.Root>
      <Dialog.Trigger>Open</Dialog.Trigger>
      <Dialog.Portal forceMount>
        <Dialog.Overlay forceMount />
        <Dialog.Content forceMount>
          Content
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}
When using forceMount, you’re responsible for handling the component’s presence and animations manually.

Animation Libraries

Framer Motion

Use Framer Motion with the asChild prop for advanced animations:
import * as Dialog from '@radix-ui/react-dialog';
import { motion, AnimatePresence } from 'framer-motion';

function MotionDialog() {
  const [open, setOpen] = useState(false);

  return (
    <Dialog.Root open={open} onOpenChange={setOpen}>
      <Dialog.Trigger>Open</Dialog.Trigger>
      <AnimatePresence>
        {open && (
          <Dialog.Portal forceMount>
            <Dialog.Overlay asChild forceMount>
              <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                transition={{ duration: 0.2 }}
              />
            </Dialog.Overlay>
            <Dialog.Content asChild forceMount>
              <motion.div
                initial={{ opacity: 0, scale: 0.95, y: 20 }}
                animate={{ opacity: 1, scale: 1, y: 0 }}
                exit={{ opacity: 0, scale: 0.95, y: 20 }}
                transition={{ duration: 0.2 }}
              >
                <Dialog.Title>Animated with Framer Motion</Dialog.Title>
                <Dialog.Close>Close</Dialog.Close>
              </motion.div>
            </Dialog.Content>
          </Dialog.Portal>
        )}
      </AnimatePresence>
    </Dialog.Root>
  );
}
1

Install Framer Motion

npm install framer-motion
2

Use controlled state

Control the open state to work with AnimatePresence:
const [open, setOpen] = useState(false);
3

Add forceMount

Use forceMount to keep the component mounted during animations:
<Dialog.Content asChild forceMount>
4

Wrap with motion components

Use motion.div with asChild to apply Framer Motion animations:
<Dialog.Content asChild forceMount>
  <motion.div {...motionProps}>
    {/* content */}
  </motion.div>
</Dialog.Content>

React Spring

import * as Collapsible from '@radix-ui/react-collapsible';
import { useSpring, animated } from '@react-spring/web';

function SpringCollapsible() {
  const [open, setOpen] = useState(false);
  
  const styles = useSpring({
    opacity: open ? 1 : 0,
    height: open ? 'auto' : 0,
    config: { tension: 300, friction: 25 },
  });

  return (
    <Collapsible.Root open={open} onOpenChange={setOpen}>
      <Collapsible.Trigger>Toggle</Collapsible.Trigger>
      <Collapsible.Content asChild forceMount>
        <animated.div style={styles}>
          Content with React Spring animation
        </animated.div>
      </Collapsible.Content>
    </Collapsible.Root>
  );
}

Common Animation Patterns

Slide and Fade

@keyframes slideUpAndFade {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

[data-state='open'] {
  animation: slideUpAndFade 200ms ease-out;
}

Scale and Fade

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

[data-state='open'] {
  animation: scaleIn 150ms ease-out;
}

Rotate Icon

.accordion-trigger[data-state='open'] .icon {
  transform: rotate(180deg);
  transition: transform 200ms ease-in-out;
}

Performance Tips

  • Use transform and opacity for better performance (GPU-accelerated)
  • Avoid animating width, height, or margin when possible
  • Use will-change sparingly and remove it after animations complete
  • Test animations on lower-end devices
/* Good - GPU accelerated */
.dialog-content {
  transition: opacity 200ms, transform 200ms;
}

/* Avoid - forces layout recalculation */
.dialog-content {
  transition: width 200ms, height 200ms;
}

Build docs developers (and LLMs) love