Skip to main content
react-md

CSS Transition

The CSSTransition component or useCSSTransition hook should be used when an element should have a CSS transition when it mounts, unmounts, or dynamically based on some flag. The transitions are built into existing react-md components like the Dialog, Menu, and Overlay.

Some existing transitions can be found at:

Check out the useTransition hook for additional options and info around the transition stages.

Simple CSSTransition Example

To create a CSS Transition:

The transitionable component must accept a ref and a className.

I will have different styles!
"use client";

import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { cssUtils } from "@react-md/core/cssUtils";
import { CSSTransition } from "@react-md/core/transition/CSSTransition";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement } from "react";

import styles from "./SimpleCSSTransitionExample.module.scss";

export default function SimpleCSSTransitionExample(): ReactElement {
  const { toggled: transitionIn, toggle } = useToggle();

  return (
    <>
      <CSSTransition
        transitionIn={transitionIn}
        timeout={{
          enter: 2000,
          exit: 1500,
        }}
        classNames={{
          enter: styles.enter,
          enterActive: styles.enterActive,
          enterDone: styles.enterDone,
          exit: styles.exit,
          exitActive: styles.exitActive,
          exitDone: styles.exitDone,
        }}
      >
        <Box
          fullWidth
          align="center"
          justify="center"
          className={cssUtils({ outlineColor: "current-color" })}
        >
          I will have different styles!
        </Box>
      </CSSTransition>
      <Button onClick={toggle}>Toggle</Button>
    </>
  );
}

Press Enter to start editing.

@use "everything";

.enter {
  color: everything.$green-900;
}

.enterActive {
  color: everything.$green-100;
  transition: color 2s everything.$deceleration-timing-function;
}

.enterDone {
  // optional
  color: everything.$green-100;
}

.exit {
  color: everything.$red-700;
}

.exitActive {
  color: everything.$red-300;
  transition: color 1.5s everything.$acceleration-timing-function;
}

.exitDone {
  // optional
  color: everything.$red-300;
}

Press Enter to start editing.

Simple useCSSTransition Example

This is the same example as above, but using the useCSSTransition hook instead of the CSSTransition component.

I will have different styles!
"use client";

import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { cssUtils } from "@react-md/core/cssUtils";
import { useCSSTransition } from "@react-md/core/transition/useCSSTransition";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement } from "react";

import styles from "./SimpleUseCSSTransitionExample.module.scss";

export default function SimpleUseCSSTransitionExample(): ReactElement {
  const { toggled: transitionIn, toggle } = useToggle();
  const { elementProps } = useCSSTransition({
    transitionIn,
    timeout: {
      enter: 2000,
      exit: 1500,
    },
    classNames: {
      enter: styles.enter,
      enterActive: styles.enterActive,
      enterDone: styles.enterDone,
      exit: styles.exit,
      exitActive: styles.exitActive,
      exitDone: styles.exitDone,
    },
    className: cssUtils({ outlineColor: "current-color" }),
  });

  return (
    <>
      <Box {...elementProps} fullWidth align="center" justify="center">
        I will have different styles!
      </Box>
      <Button onClick={toggle}>Toggle</Button>
    </>
  );
}

Press Enter to start editing.

@use "everything";

.enter {
  color: everything.$green-900;
}

.enterActive {
  color: everything.$green-100;
  transition: color 2s everything.$deceleration-timing-function;
}

.enterDone {
  // optional
  color: everything.$green-100;
}

.exit {
  color: everything.$red-700;
}

.exitActive {
  color: everything.$red-300;
  transition: color 1.5s everything.$acceleration-timing-function;
}

.exitDone {
  // optional
  color: everything.$red-300;
}

Press Enter to start editing.

Temporary Elements Transitions

The CSS Transition really shines for temporary elements that should have enter and exit transitions since it also returns a rendered flag that will be false once the exit transition has completed. Enable the temporary prop on the CSSTransition component or useCSSTransition hook to enable this feature.

"use client";

import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { useCSSTransition } from "@react-md/core/transition/useCSSTransition";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement } from "react";

import styles from "./TemporaryElementsTransitionsExample.module.scss";

export default function TemporaryElementsTransitionsExample(): ReactElement {
  const { toggled, toggle } = useToggle();
  const { elementProps, rendered } = useCSSTransition({
    transitionIn: toggled,
    temporary: true,
    timeout: 150,
    classNames: {
      enter: styles.enter,
      enterActive: styles.enterActive,
      exit: styles.exit,
      exitActive: styles.exitActive,
    },
  });

  return (
    <Box stacked disablePadding>
      <Button onClick={toggle}>Toggle</Button>
      {rendered && <div {...elementProps}>I am a temporary element!</div>}
    </Box>
  );
}

Press Enter to start editing.

.enter {
  opacity: 0;
}

.enterActive {
  opacity: 1;
  transition: opacity 0.15s;
}

.exit {
  opacity: 1;
}

.exitActive {
  opacity: 0;
  transition: opacity 0.15s;
}

Press Enter to start editing.

Display None Example

Instead of unmounting the component while transitionIn is false, another option is to hide it in the DOM by using display: none. Enable the exitedHidden option instead of temporary.

The main benefit to this behavior is that the state of the temporary element will not reset since it is never unmounted.

I am a temporary element!
"use client";

import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { useCSSTransition } from "@react-md/core/transition/useCSSTransition";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement } from "react";

import styles from "./TemporaryElementsTransitionsExample.module.scss";

export default function DisplayNoneExample(): ReactElement {
  const { toggled, toggle } = useToggle();
  const { elementProps } = useCSSTransition({
    transitionIn: toggled,
    exitedHidden: true,
    timeout: 150,
    classNames: {
      enter: styles.enter,
      enterActive: styles.enterActive,
      exit: styles.exit,
      exitActive: styles.exitActive,
    },
  });

  return (
    <Box stacked disablePadding>
      <Button onClick={toggle}>Toggle</Button>
      <div {...elementProps}>I am a temporary element!</div>
    </Box>
  );
}

Press Enter to start editing.

.enter {
  opacity: 0;
}

.enterActive {
  opacity: 1;
  transition: opacity 0.15s;
}

.exit {
  opacity: 1;
}

.exitActive {
  opacity: 0;
  transition: opacity 0.15s;
}

Press Enter to start editing.

Appear Transition

The enter transition will not occur by default if a component mounts with transitionIn set to true since this is considered an "appear" transition. If this behavior is desired, enable the appear option which defaults to reusing all the same styles and timeouts as the enter stages.

    "use client";
    
    import { Box } from "@react-md/core/box/Box";
    import { Button } from "@react-md/core/button/Button";
    import { List } from "@react-md/core/list/List";
    import { ListItem } from "@react-md/core/list/ListItem";
    import { CSSTransition } from "@react-md/core/transition/CSSTransition";
    import { type ReactElement, useState } from "react";
    
    import styles from "./AppearTransitionExample.module.scss";
    
    export default function AppearTransitionExample(): ReactElement {
      const [count, setCount] = useState(0);
      return (
        <Box stacked disablePadding>
          <Button
            onClick={() => {
              setCount((prevCount) => {
                // just so it doesn't render too many
                if (prevCount === 9) {
                  return 0;
                }
    
                return prevCount + 1;
              });
            }}
          >
            Add
          </Button>
          <List>
            {Array.from({ length: count }, (_, i) => (
              <CSSTransition
                key={i}
                appear
                timeout={150}
                classNames={{
                  enter: styles.enter,
                  enterActive: styles.enterActive,
                }}
                transitionIn
              >
                <ListItem primaryText={`Item ${i + 1}`} />
              </CSSTransition>
            ))}
          </List>
        </Box>
      );
    }
    

    Press Enter to start editing.

    .enter {
      opacity: 0;
    }
    
    .enterActive {
      opacity: 1;
      transition: opacity 0.15s;
    }
    

    Press Enter to start editing.

    CSS-Only Appear Transition

    If the component only requires a transition on first mount, it is recommended to use a CSS-only transition instead to reduce the javascript bundle size. Here is the same example as above but using css instead of the useCSSTransition hook.

      "use client";
      
      import { Box } from "@react-md/core/box/Box";
      import { Button } from "@react-md/core/button/Button";
      import { List } from "@react-md/core/list/List";
      import { ListItem } from "@react-md/core/list/ListItem";
      import { type ReactElement, useState } from "react";
      
      import styles from "./CSSOnlyAppearTransitionExample.module.scss";
      
      export default function CSSOnlyAppearTransitionExample(): ReactElement {
        const [count, setCount] = useState(0);
        return (
          <Box stacked disablePadding>
            <Button
              onClick={() => {
                setCount((prevCount) => {
                  // just so it doesn't render too many
                  if (prevCount === 9) {
                    return 0;
                  }
      
                  return prevCount + 1;
                });
              }}
            >
              Add
            </Button>
            <List>
              {Array.from({ length: count }, (_, i) => (
                <ListItem
                  key={i}
                  className={styles.item}
                  primaryText={`Item ${i + 1}`}
                />
              ))}
            </List>
          </Box>
        );
      }
      

      Press Enter to start editing.

      .item {
        animation: appear 0.15s;
      }
      
      @keyframes appear {
        from {
          opacity: 0;
        }
      
        to {
          opacity: 1;
        }
      }
      

      Press Enter to start editing.