Creating a Reusable Bulletproof React IconButton Component
Tags: react, typescript, styledcomponents, ui
Published: 2023-07-12 09:52:02

✨ [**Watch on YouTube**](https://youtu.be/zliCty-G8nA)  |  🐙 [GitHub](https://github.com/RodionChachura/reactkit/blob/main/pages/icon-button.tsx)  |  🎮 [Demo](https://reactkit.radzion.com/icon-button)

Let me share a reusalbe bulletproof React component for displaying a clickable icon - the `IconButton`.

![Example from ReactKit](https://cdn-images-1.medium.com/proxy/1*9CvVb_sTSrwPPklUR5NYvA.jpeg)

## The IconButton component

The `Container` component is a styled button that will have different styles based on `size` and `kind` props. Here we have `s`, `m`, `l` sizes, and `regular`, `secondary`, `alert` kinds. You can have more or less depending on your needs.

```tsx
import { ComponentProps, Ref, forwardRef } from "react"
import styled from "styled-components"
import { defaultTransitionCSS } from "../animations/transitions"
import { centerContentCSS } from "../utils/centerContentCSS"
import { getCSSUnit } from "../utils/getCSSUnit"
import { getSameDimensionsCSS } from "../utils/getSameDimensionsCSS"
import { matchColor } from "../theme/getters"
import { match } from "lib/shared/utils/match"
import { interactiveCSS } from "../utils/interactiveCSS"

export const iconButtonSizes = ["s", "m", "l"] as const
export type IconButtonSize = (typeof iconButtonSizes)[number]

export const iconButtonKinds = ["regular", "secondary", "alert"] as const
export type IconButtonKind = (typeof iconButtonKinds)[number]

const sizeRecord: Record<IconButtonSize, number> = {
  s: 24,
  m: 32,
  l: 40,
}

interface ContainerProps {
  size: IconButtonSize
  kind: IconButtonKind
}

const Container = styled.button<ContainerProps>`
  all: unset;
  ${interactiveCSS};
  position: relative;
  ${centerContentCSS};
  ${({ size }) => getSameDimensionsCSS(sizeRecord[size])};
  font-size: ${({ size }) => `calc(${getCSSUnit(sizeRecord[size] * 0.6)})`};
  border-radius: 8px;
  ${defaultTransitionCSS};

  color: ${matchColor("kind", {
    regular: "text",
    secondary: "text",
    alert: "alert",
  })};

  background: ${({ kind, theme: { colors } }) =>
    match(kind, {
      regular: () => colors.mist,
      secondary: () => colors.transparent,
      alert: () => colors.alert.getVariant({ a: (a) => a * 0.12 }),
    }).toCssValue()};

  :hover {
    background: ${({ kind, theme: { colors } }) =>
      match(kind, {
        regular: () => colors.mist,
        secondary: () => colors.mist,
        alert: () => colors.alert.getVariant({ a: (a) => a * 0.24 }),
      }).toCssValue()};

    color: ${matchColor("kind", {
      regular: "contrast",
      secondary: "contrast",
      alert: "alert",
    })};
  }
`

export interface IconButtonProps extends ComponentProps<typeof Container> {
  icon: React.ReactNode
  size?: IconButtonSize
  kind?: IconButtonKind
  title: string
}

export const IconButton = forwardRef(function IconButton(
  { size = "m", kind = "regular", icon, ...rest }: IconButtonProps,
  ref: Ref<HTMLButtonElement> | null
) {
  return (
    <Container kind={kind} ref={ref} size={size} {...rest}>
      {icon}
    </Container>
  )
})
```

We remove all the default styles from the button with the `all: unset` CSS rule. Then we apply the `interactiveCSS` that will add `cursor: pointer`. To center icon inside we turn it in a flexbox element with `alignItems: center` and `justifyContent: center` using the `centerContentCSS`. To make it a square we use the `getSameDimensionsCSS` helper, and to make the icon fit inside we set the `font-size` to `60%` of the size.

I use `HSLA` color format through my apps, and you can learn more about it in my [blog post](https://radzion.com/blog/hsla-color). Here we use the `matchColor` helper to get the color based on the `kind` prop. We do the same for the `background`, but here we want to maninulate color parameters so we have to write a bit more code. We use the same approach to update `color` and `background` on hover.

The `IconButton` component will receive the same properties as the container, but also `icon` and `title` props. The `icon` prop is a React node that will be rendered inside the button. In my apps I have icons as a regular React component and you can learn more about the setup [here](https://radzion.com/blog/svg). We make the `title` prop required for accessibility purposes, since there is no text inside the button we need to have some explanation for the action.

There are situations where you want the icon to be aligned with the content and have hover effect go beyond the boundaries of the element, to do that you can use the `Hoverable` component that positions highlight absolute, and you can learn more about it in my [blog post](https://radzion.com/blog/hoverable).