Skip to content

Instantly share code, notes, and snippets.

@romannurik
Last active September 18, 2023 01:04
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save romannurik/8d6681ccf55dc44f09caf63ec13945da to your computer and use it in GitHub Desktop.
Save romannurik/8d6681ccf55dc44f09caf63ec13945da to your computer and use it in GitHub Desktop.
A simple expand/collapse animation using React
import React, {useRef, useLayoutEffect} from 'react';
const [DURMIN, DURMAX] = [0.1, .8];
const dur = f => Math.min(DURMAX, (1 - f) * DURMIN + f * DURMAX);
export function Expando({className, children, open}) {
open = !!open;
let node = useRef();
let lastOpen = useRef(open);
let duration = useRef(.5);
let animPhase = ({prep, start, end}) => {
if (end) {
node.current.style.transition = '';
node.current.style.maxHeight = open ? 'none' : 0;
} else { // prep or start
let contentHeight = node.current.firstChild.offsetHeight;
duration.current = dur(contentHeight / 1000);
node.current.style.maxHeight = ((!!prep == open) ? 0 : contentHeight) + 'px';
node.current.style.transition = `max-height ${duration.current}s ease` +
(open ? '' : `, visibility 0s linear ${duration.current}s`);
node.current.scrollTop; // force reflow
}
};
if (node.current && open != lastOpen.current) {
animPhase({prep:true}); // just before render
}
lastOpen.current = open;
useLayoutEffect(() => { // just after render
animPhase({start:true});
let end = ev => ev.target == node.current && animPhase({end:true});
node.current.addEventListener('transitionend', end);
return () => node.current.removeEventListener('transitionend', end);
}, [open]);
return <div className={className} ref={node}
style={{
overflow: 'hidden',
visibility: open ? 'visible' : 'hidden'
}}>
<div style={{
transform: open ? 'none' : 'translateY(-100%)',
transition: `transform ${duration.current}s ease`,
}}>{children}</div>
</div>;
}
import {Expando} from './Expando.jsx';
function App(props) {
let [open, setOpen] = React.useState(false);
return <React.Fragment>
<button onClick={() => setOpen(!open)}>
{open ? 'Close' : 'Open'}
</button>
<Expando open={open}>
Child<br/>
Content<br/>
Here<br/>
</Expando>
</React.Fragment>;
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment