DEV Community

Ramil
Ramil

Posted on

Animate height: auto with removing content from DOM in React.js

During a recent project, I encountered a seemingly straightforward task: animating content expansion upon clicking a title.

However, implementing this in React posed challenges, especially since the component had to accommodate dynamic content with unknown heights.

Problem №1

Unfortunately, we still can't just write something like

.content {
  height: 0;
  transition: height .3s ease;
}

.content.expanded {
  height: auto;
}
Enter fullscreen mode Exit fullscreen mode

because it won't work.

Of course, we can implement this using JavaScript, but I decided not to retreat and still try to find ways to use only CSS for animation.

success kid meme

And I was lucky, because I was found examples of animation implementation using only css grid. Here is one of these wonderful articles.

Solution №1

Great, we have an example of animation, so all we have to do is implement it in react.
Simple implementation will look something like this:

.content {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s ease-out;
}

.content.expanded {
  grid-template-rows: 1fr;
}

.inner {
  overflow: hidden;
}
Enter fullscreen mode Exit fullscreen mode
const Expandable = ({
  children,
  defaultExpaded = false,
  title,
}: {
  children: ReactNode;
  title: ReactNode;
  defaultExpaded?: boolean;
}) => {
  const [expanded, setExpanded] = useState(defaultExpaded);

  return (
    <>
      <button onClick={() => setExpanded(!expanded)}>{title}</button>
      <div className={`content ${expanded ? 'expanded' : ''}`}>
        <div className="inner">{children}</div>
      </div>
    </>
  );
};

function App() {
  return (
    <Expandable title="Logo" defaultExpaded>
      <img src={reactLogo} className="logo react" alt="React logo" />
    </Expandable>
  );
}
Enter fullscreen mode Exit fullscreen mode

So everything works as expected so we can submit the task for code review, drink a cup of tea and take on a new task.

cup of tea with KEEP CALM AND DRINK TEA text

After some time during the code review, I received a question:

Can we make it so that if the content is collapsed, then we do not render the DOM that is hidden?

Good question, so let's think about it

Problem №2

It would seem that we could simply add a check and render children only if expanded === true:

<>
  <button onClick={() => setExpanded(!expanded)}>{title}</button>
  <div className={`content ${expanded ? 'expanded' : ''}`}>
    {/* Add check for expanded */}
    <div className="inner">{expanded && children}</div>
  </div>
</>
Enter fullscreen mode Exit fullscreen mode

but this won't work, because whenexpanded === false we will not have any content, which means that the animation will not happen.

Solution №2

So, we know that the problem is that during animation we have no content. It means we need to come up with something that will hide the content after our animation ends.

Thinking meme

Fortunately, we have an event that will be called exactly after the animation ends. Because we use css tranision property for animation, we can simply add an onTransitionEnd event handler to the animated div and we alsow will need an additional variable that will control the visibility.

So let's put this functionality into a custom hook for not to write logic inside the component:

const useExpand = (defaultExpanded: boolean) => {
  const [visible, setVisible] = useState(defaultExpanded);
  const [expanded, setExpanded] = useState(defaultExpanded);

  const toggle = useCallback(() => {
    if (!expanded) {
      setVisible(true);
      setExpanded(true);
    } else {
      setExpanded(false);
    }
  }, [visible, expanded]);

  const onToggled = useCallback(() => {
    if (visible && !expanded) {
      setVisible(false);
    }
  }, [expanded, visible]);

  return { toggle, expanded, visible, onToggled };
};
Enter fullscreen mode Exit fullscreen mode

Let's see on useExpand hook closely.
We declared an additional variable visible, which will be responsible for whether to delete the DOM or not. To control toggle we will use the toggle function, and the animation end event will be handled by the onToggled function.

Now we can use this hook in our Expandable component

const Expandable = ({
  children,
  defaultExpaded = false,
  title,
}: {
  children: ReactNode;
  title: ReactNode;
  defaultExpaded?: boolean;
}) => {
  const content = useExpand(defaultExpaded);

  return (
    <>
      <button onClick={content.toggle}>{title}</button>
      <div
        className={`content ${content.expanded ? 'expanded' : ''}`}
        onTransitionEnd={content.onToggled}
      >
        <div className="inner">{content.visible && children}</div>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Note: Because in this example the animation occurs through css transition, we attach the onToggled function to the onTransitionEnd event. If we used css animation the event would be onAnimationEnd.

Here is live example of code

Recap

So, in this article I wanted to show how you can implement height animation and hide content from the DOM when the content is collapsed.
We wrote a hook that can be used in any other component, keeping in mind the necessary markup and handlers for the corresponding events

Special thanks to my colleague Evgeniy for his invaluable contributions to improving readability and code quality

Top comments (0)