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;
}
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.
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;
}
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>
);
}
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.
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>
</>
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.
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 };
};
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>
</>
);
};
Note: Because in this example the animation occurs through css transition, we attach the
onToggled
function to theonTransitionEnd
event. If we used css animation the event would beonAnimationEnd
.
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)