Creating an Accordion Component in React with Typescript and TailwindCSS
Tags: hooks, react, tailwindcss, typescript
Published: 2021-05-22 10:53:49

---
title: Creating an Accordion Component in React with Typescript and TailwindCSS
published: true
date: 2021-05-22 00:00:00 UTC
tags: hooks, react, tailwindCSS, typescript
canonical_url: https://bionicjulia.com/blog/creating-accordion-component-react-typescript-tailwind
---

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](https://tailwindcss.com/docs) utility classes (these classes basically sets the scene, telling the component that at some point, we're going to want to animate something).

```jsx
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.

```jsx
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(active === false ? true : false)
    // @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.

```jsx
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(active === false ? true : false)
    // @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.

```jsx
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(active === false ? true : false)
    // @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](https://twitter.com/bionicjulia) or Instagram [@bionicjulia](https://instagram.com/bionicjulia).