How To Make Tooltip React Component with Arrow using Floating UI
Tags: react, ui, typescript, floatingui
Published: 2023-07-01 12:17:04

✨ [**Watch on YouTube**](https://youtu.be/gt-29kdEwtA)  |  🐙 [GitHub](https://github.com/RodionChachura/reactkit)  |  🎮 [Demo](https://reactkit.radzion.com/tooltip)

Let's make a beautiful reusable tooltip component with React and Floating UI. It will automatically reposition itself when there is not enough space, have an arrow pointing to a referencing element, and look good in both dark and light modes.

![Example of the tooltip component](https://cdn-images-1.medium.com/proxy/1*JmdiUXaT5XiqhKAtR5DMOA.jpeg)

## Tooltip React Component

The `Tooltip` component receives a `content` prop with the text to display inside the tooltip, a `renderOpener` prop with a function that returns a React element that will open the tooltip when hovered, and a `placement` prop with the preferred placement of the tooltip relative to the opener.

```tsx
import { ReactNode, useRef, useState } from "react"
import {
  ReferenceType,
  offset,
  shift,
  flip,
  useFloating,
  autoUpdate,
  useInteractions,
  useHover,
  useFocus,
  useDismiss,
  useRole,
  arrow,
  FloatingArrow,
  useTransitionStyles,
  Placement,
} from "@floating-ui/react"
import styled from "styled-components"
import { getColor } from "./theme/getters"

export interface RenderOpenerProps extends Record<string, unknown> {
  ref: (node: ReferenceType | null) => void
}

interface TooltipProps {
  content?: ReactNode
  renderOpener: (props: RenderOpenerProps) => ReactNode
  placement?: Placement
}

const Container = styled.div`
  border-radius: 8px;
  background: ${getColor("contrast")};
  color: ${getColor("background")};
  padding: 12px;
  font-size: 14px;
  font-weight: 500;
  max-width: 320px;
`

const Arrow = styled(FloatingArrow)`
  fill: ${getColor("contrast")};
`

export const Tooltip = ({ content, renderOpener, placement }: TooltipProps) => {
  const [isOpen, setIsOpen] = useState(false)

  const {
    refs: { setReference, setFloating },
    floatingStyles,
    context,
  } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    placement,
    middleware: [
      offset(12),
      flip(),
      shift(),
      arrow({
        element: arrowRef,
      }),
    ],
    whileElementsMounted: autoUpdate,
  })

  const arrowRef = useRef(null)

  const hover = useHover(context, { move: false })
  const focus = useFocus(context)
  const dismiss = useDismiss(context)
  const role = useRole(context, { role: "tooltip" })

  const { getReferenceProps, getFloatingProps } = useInteractions([
    hover,
    focus,
    dismiss,
    role,
  ])

  const { styles: transitionStyles } = useTransitionStyles(context, {
    initial: {
      opacity: 0,
      transform: "scale(0.8)",
    },
  })

  return (
    <>
      {renderOpener({ ref: setReference, ...getReferenceProps() })}
      {isOpen && content && (
        <div
          ref={setFloating}
          style={{ ...floatingStyles, zIndex: 1 }}
          {...getFloatingProps()}
        >
          <Container style={transitionStyles}>
            <Arrow tipRadius={2} height={8} ref={arrowRef} context={context} />
            {content}
          </Container>
        </div>
      )}
    </>
  )
}
```

We store the opened/closed state in the useState hook. All the magic for positining the floating element lies in the `useFloating` hook. We pass the `isOpen` state and the `setIsOpen` function to the hook, so it can update the state when the user hovers over the opener, we also propagate the `placement` prop to the hook, so it knows where to position the tooltip, and middleware for modifying the position of the tooltip. In this case offset creates a distance between the opener and tooltip, `flip` and `shift` make sure that the tooltip gets repositioned when there is not enough space, and `arrow` creates an arrow pointing to the opener.

To trigger appearance on hover we rely on the `useHover` hook, to also display the tooltip on focus we have the `useFocus` hook, and to hide the tooltip on escape key press - the `useDismiss` hook. To get properties for accessibility purposes we apply the `useRole` hook. To combine all those effects we use the `useInteractions` hook.

To apply animation for appearance there is the `useTransitionStyles` hook. It returns a `styles` object that we can apply to the tooltip container. To make tooltip stand out on both dark and light mode we reverse colors by making text color the same as the background color and vice versa.

Finally we render the opener and the tooltip itself. We pass all the props and ref to the `renderOpener` function, and when the `isOpen` is `true` we render the tooltip itself. We apply position styles to a regular div, while passing `transitionStyles` to the tooltip container.