Avatar

๐Ÿ‘‹๐Ÿป, I'm Julia.

Creating an Accordion Component in React with Typescript and TailwindCSS

#hooks #react #tailwind #typescript

5 min read

Update (May 2022): - I've added a blog post for those who don't use TailwindCSS, and want to create a React accordion component with straight CSS.


I don't currently use any external UI libraries in my React app, so when the designs called for an accordion component, I decided to figure out how easy it would be to build one from scratch. Turns out - it ain't too bad. ๐Ÿ˜„

Building blocks

The basic building blocks you'll need to build up the accordion are:

  • Some kind of chevron icon (I used an SVG)
  • State variables for:
    • Whether the accordion is active (open) or not active (closed).
    • Depending on the active state, what the height of the entire accordion should be.
    • The rotation angle of the chevron icon as the accordion transitions from an open to closed state (and vice versa).

The two props I'd like to pass into my Accordion component are a title (the text that's seen when the accordion is closed) and content (the additional text that's seen when the accordion is open).

If you're not familiar with the useState React hook, the values in the parentheses are the initial values for the state variable, so for e.g. const active's starting value is false (closed). The transform duration-700 ease refers to TailwindCSS utility classes (these classes basically sets the scene, telling the component that at some point, we're going to want to animate something).

import React, { useState } from 'react'

interface AccordionProps {
  title: React.ReactNode
  content: React.ReactNode
}

export const Accordion: React.FC<AccordionProps> = ({ title, content }) => {
  const [active, setActive] = useState(false)
  const [height, setHeight] = useState('0px')
  const [rotate, setRotate] = useState('transform duration-700 ease')

  // ...
}

Layer 1

Next layer up is having some kind of toggle function that sets the active state to either true or false. This function should also set the height and rotation depending on what the active state is.

Note that we've yet to determine the height when our active state is true. That comes in the next layer below.

import React, { useState } from 'react'

interface AccordionProps {
  title: React.ReactNode
  content: React.ReactNode
}

export const Accordion: React.FC<AccordionProps> = ({ title, content }) => {
  const [active, setActive] = useState(false)
  const [height, setHeight] = useState('0px')
  const [rotate, setRotate] = useState('transform duration-700 ease')

	function toggleAccordion() {
    setActive((prevState) => !prevState)
    // @ts-ignore
    setHeight(active ? '0px' : `${someHeightYetToBeDetermined}px`)
    setRotate(active ? 'transform duration-700 ease' : 'transform duration-700 ease rotate-180')
  }

  // ...
}

Layer 2

Going another layer up, we need some way of targeting the DOM, where the inner content of the accordion will reside. An easy way of doing this is through the helpful useRef hook that React gives us, that allows us to specifically target (in my case) a <div> where my content will sit.

To make this work, I used inline CSS to set a maxHeight attribute which equates to the height state variable I introduced in Layer 1 above. i.e. if it's not active, the height will be 0 (hidden). We can now also refer to the contentSpace to determine what the height should be when the accordion is active, using ${contentSpace.current.scrollHeight}px.

Note also that I wanted a nice opening and closing animation effect, so I used TailwindCSS to set an ease-in-out effect.

import React, { useRef, useState } from 'react'
import { appConfig } from '../../../../appConfig'

interface AccordionProps {
  title: React.ReactNode
  content: React.ReactNode
}

export const Accordion: React.FC<AccordionProps> = ({ title, content }) => {
	// ...

  const contentSpace = useRef<HTMLDivElement>(null)

  function toggleAccordion() {
    setActive((prevState) => !prevState)
    // @ts-ignore
    setHeight(active ? '0px' : `${contentSpace.current.scrollHeight}px`)
    setRotate(active ? 'transform duration-700 ease' : 'transform duration-700 ease rotate-180')
  }

  return (
		<div
        ref={contentSpace}
        style={{ maxHeight: `${height}` }}
        className="overflow-auto transition-max-height duration-700 ease-in-out"
      >
      <div className="pb-10">{content}</div>
    </div>
	)
}

Putting it all together

All that's left now is to pull all of our building blocks together. Here's what our complete Accordion component looks like.

The main things to note here are:

  • That I created a button, within which the title prop sits along with my chevron icon.
  • I added an onClick handler to this button which I hooked up to the toggleAccordion function we created in Level 1.
  • I added the rotate state variable to the classNames for my chevron icon. These are the Tailwind classes that rotates the icon depending on the active state of the accordion.
import React, { useRef, useState } from 'react'
import { appConfig } from '../../../../appConfig'

interface AccordionProps {
  title: React.ReactNode
  content: React.ReactNode
}

export const Accordion: React.FC<AccordionProps> = ({ title, content }) => {
  const [active, setActive] = useState(false)
  const [height, setHeight] = useState('0px')
  const [rotate, setRotate] = useState('transform duration-700 ease')

  const contentSpace = useRef(null)

  function toggleAccordion() {
    setActive((prevState) => !prevState)
    // @ts-ignore
    setHeight(active ? '0px' : `${contentSpace.current.scrollHeight}px`)
    setRotate(active ? 'transform duration-700 ease' : 'transform duration-700 ease rotate-180')
  }

  return (
    <div className="flex flex-col">
      <button
        className="py-6 box-border appearance-none cursor-pointer focus:outline-none flex items-center justify-between"
        onClick={toggleAccordion}
      >
        <p className="inline-block text-footnote light">{title}</p>
        <img
          src={`${appConfig.publicUrl}/img/icons/chevron-up.svg`}
          alt="Chevron icon"
          className={`${rotate} inline-block`}
        />
      </button>
      <div
        ref={contentSpace}
        style={{ maxHeight: `${height}` }}
        className="overflow-auto transition-max-height duration-700 ease-in-out"
      >
        <div className="pb-10">{content}</div>
      </div>
    </div>
  )
}

And that's it! What did you think? Any ways I can improve this? Let's chat on Twitter @bionicjulia or Instagram @bionicjulia.

ยฉ 2016-2024 Julia Tan ยท Powered by Next JS.