React animated accordion from scratch

Framework/Tools

In this post, I have shown how to build an animated accordion without using any external library. Using getComputedStyle we can get the height of each accordion and animate accordingly with CSS.

This is the functional view of our end product.

This is the basic starting code of the Accordion component

export default Accordion = ({content}) => {
  const [accordion, setAccordion] = useState(false)
  
  return (
    <>
      <div className="accordion">
        <header onClick={() => setAccordion(!accordion)}>
          <h2>{content.question}</h2>
          {/* toggle icon */}
          <svg className={accordion ? 'open' : ''} x="0px" y="0px" viewBox="0 0 330 330" width="20" height="20">
            <path d="M325.607,79.393c-5.857-5.857-15.355-5.858-21.213,
            0.001l-139.39,139.393L25.607,79.393  c-5.857-5.857-15.355-5.858-21.213,
            0.001c-5.858,5.858-5.858,15.355,0,21.213l150.004,150c2.813,2.813,6.628,
            4.393,10.606,4.393  s7.794-1.581,10.606-4.394l149.996-150C331.465,
            94.749,331.465,85.251,325.607,79.393z"/>
          </svg>
        </header>
        <div className="collapsible-content">
          <div className="content-wrapper">
            <p>{content.answer}</p>
          </div>
        </div>
      </div>
    </>
  )
}

The state accordion will keep track whether the content is collapsed or not. Initially, it's collapsed. To keep track of height, we will use another state height and initially set it to 0.

const [height, setHeight] = useState(0)

We will need to add a ref to the div with class name collapsible-content to access it's DOM property.

const collapsibleContent = useRef()
<div ref={collapsibleContent} className="collapsible-content">
  <div className="content-wrapper">
    ...
  </div>
</div>

Since we will be animating the height, we need to calculate the height in 'px' when the content size is 'auto'

const toggleCollapsible = () => {
    const el = collapsibleContent.current
    if (tab) {
      el.style.height = 'auto'
      const newHeight = getComputedStyle(el).height
      setHeight(newHeight)
      el.style.height = 0
    } else {
      setHeight(0)
    }
}

When accordion state is true-

  1. First we are setting the collapsibleContent height to 'auto'
  2. Using getComputedStyle we are calculating the height the content occupies if set to 'auto'
  3. Storing that value in height and then setting the collapsibleContent height back to 0, because now we will animate from 0 to newHeight.

toggleCollapsible function will be called anytime accordion state is changed.

useEffect(() => {
    toggleCollapsible()
  }, [accordion])

We will also update collapsibleContent everytime the state of height changes.

useEffect(() => {
    collapsibleContent.current.style.height = height
  }, [height])

For animation, transition property is added to collapsible-content class

.collapsible-content {
  transition: 0.4s height ease-out;
  overflow: hidden;
}

Here is the complete code of Accordion component

const Accordion = ({content}) => {
  const [accordion, setAccordion] = useState(false)
  const collapsibleContent = useRef()
  const [height, setHeight] = useState(0)

  useEffect(() => {
    toggleCollapsible()
  }, [accordion])
  
  useEffect(() => {
    collapsibleContent.current.style.height = height
  }, [height])


  const toggleCollapsible = () => {
    const el = collapsibleContent.current
    if (accordion) {
      el.style.height = 'auto'
      const newHeight = getComputedStyle(el).height
      el.style.height = 0
      setHeight(newHeight)
    } else {
      setHeight(0)
    }
  }
    return (
     <>
      <div className="accordion">
        <header onClick={() => setAccordion(!accordion)}>
          <h2>{content.question}</h2>
          <svg className={accordion ? 'open' : ''} x="0px" y="0px" viewBox="0 0 330 330" width="15" height="15">
            <path d="M325.607,79.393c-5.857-5.857-15.355-5.858-21.213,
            0.001l-139.39,139.393L25.607,79.393  c-5.857-5.857-15.355-5.858-21.213,
            0.001c-5.858,5.858-5.858,15.355,0,21.213l150.004,150c2.813,2.813,6.628,
            4.393,10.606,4.393  s7.794-1.581,10.606-4.394l149.996-150C331.465,
            94.749,331.465,85.251,325.607,79.393z"/>
          </svg>
        </header>
        <div ref={collapsibleContent} className="collapsible-content">
          <div className="content-wrapper">
            <p>{content.answer}</p>
          </div>
        </div>
      </div>
    </>
  )
}