custom tooltip component in React

In big and complex projects creating custom components for everything is very common. Sometime we don’t fulfil our needs by using the native solutions. In those cases creating our own components is the only solution. In this article we will gonna create a custom tooltip component in React.

Why custom tooltip ?

Before starting the code there is one most important question. Why we need a custom tooltip instead of using a native solution.

The answer is simple we need something that can fit according to our needs. The custom background, styling, arrow icons, box-shadow etc etc. This can only be achieved by a custom solution.

Tooltip component

We will gonna create a component using hook which gonna have few props for example

  • children- for attaching a tooltip on them.
  • content- the tooltip text.
  • hide- function to hide the tooltip.
  • show- boolean value for tooltip state.
  • childRef- in case we want to focus child while showing tooltip.

Our render function will gonna look like this.

  <div
      className="Tooltip-Wrapper"
      ref={ref}
    >
      {children}
      {show && (
        <div className={`Tooltip-Tip ${width > 900 ? 'right' : 'bottom'}`}>
          {content}
        </div>
      )}
    </div>

A simple useEffect logic to focus the the child.

  useEffect(() => {
    if (!isEmpty(childRef) && show) {
      childRef.current.focus();
    }
  }, [show]);

We also need two hook functions.

useOnClickOutside this hook will gonna tell us whenever user clicks outside.

import { useEffect } from 'react';

export const useOnClickOutside = (ref, handler) => {
  useEffect(
    () => {
      const listener = (event) => {
        // Do nothing if clicking ref's element or descendent elements
        if (!ref.current || ref.current.contains(event.target)) {
          return;
        }
        handler(event);
      };

      document.addEventListener('mousedown', listener);
      document.addEventListener('touchstart', listener);

      return () => {
        document.removeEventListener('mousedown', listener);
        document.removeEventListener('touchstart', listener);
      };
    },
    // Add ref and handler to effect dependencies
    // It's worth noting that because passed in handler is a new ...
    // ... function on every render that will cause this effect ...
    // ... callback/cleanup to run every render. It's not a big deal ...
    // ... but to optimize you can wrap handler in useCallback before ...
    // ... passing it into this hook.
    [ref, handler],
  );
};

useWindowSize hook for checking the screen size so that we can make it responsive (can be done using mediaQuery).

import { useState, useEffect } from 'react';

export const useWindowSize = () => {
  const isSSR = typeof window !== 'undefined';
  const [windowSize, setWindowSize] = useState({
    width: isSSR ? 1200 : window.innerWidth,
    height: isSSR ? 800 : window.innerHeight,
  });

  function changeWindowSize() {
    setWindowSize({ width: window.innerWidth, height: window.innerHeight });
  }

  useEffect(() => {
    window.addEventListener('resize', changeWindowSize);

    return () => {
      window.removeEventListener('resize', changeWindowSize);
    };
  }, []);

  return windowSize;
};

Some styling to make our tooltip good.

/* Custom properties */
:root {
    --tooltip-text-color: white;
    --tooltip-background-color: #F86A6A;
    --tooltip-margin: 12px;
    --tooltip-arrow-size: 6px;
  }
  
  /* Wrapping */
  .Tooltip-Wrapper {
    display: inline-block;
    position: relative;
    width: 100%;
  }
  
  /* Absolute positioning */
  .Tooltip-Tip {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    padding: 8px;
    color: var(--tooltip-text-color);
    background: var(--tooltip-background-color);
    font-size: 12px;
    font-family: sans-serif;
    line-height: 1;
    z-index: 100;
    min-width: 160px;
    max-width: 160px;
  }
  
  /* CSS border triangles */
  .Tooltip-Tip::before {
    content: " ";
    left: 50%;
    border: solid transparent;
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
    border-width: var(--tooltip-arrow-size);
    margin-left: calc(var(--tooltip-arrow-size) * -1);
  }
  
  /* Absolute positioning */
  .Tooltip-Tip.top {
    top: calc(var(--tooltip-margin) * -1);
  }
  /* CSS border triangles */
  .Tooltip-Tip.top::before {
    top: 100%;
    border-top-color: var(--tooltip-background-color);
  }
  
  /* Absolute positioning */
  .Tooltip-Tip.right {
    left: calc(100% + var(--tooltip-margin));
    top: 50%;
    transform: translateX(0) translateY(-50%);
  }
  /* CSS border triangles */
  .Tooltip-Tip.right::before {
    left: calc(var(--tooltip-arrow-size) * -1);
    top: 50%;
    transform: translateX(0) translateY(-50%);
    border-right-color: var(--tooltip-background-color);
  }
  
  /* Absolute positioning */
  .Tooltip-Tip.bottom {
    bottom: calc(var(--tooltip-margin) * -1px);
  }
  /* CSS border triangles */
  .Tooltip-Tip.bottom::before {
    bottom: 100%;
    border-bottom-color: var(--tooltip-background-color);
  }
  
  /* Absolute positioning */
  .Tooltip-Tip.left {
    left: auto;
    right: calc(100% + var(--tooltip-margin));
    top: 50%;
    transform: translateX(0) translateY(-50%);
  }
  /* CSS border triangles */
  .Tooltip-Tip.left::before {
    left: auto;
    right: calc(var(--tooltip-arrow-size) * -2);
    top: 50%;
    transform: translateX(0) translateY(-50%);
    border-left-color: var(--tooltip-background-color);
  }
  

Our tooltip component will gonna look like this.

/* eslint-disable no-unused-vars */
import PropTypes from 'prop-types';
import React, { useRef, useEffect } from 'react';
import { isEmpty } from 'lodash';

import { useOnClickOutside, useWindowSize } from '../../util';

export const Tooltip = ({
  children, content, show, hide, childRef,
}) => {
  const ref = useRef();
  useOnClickOutside(ref, hide);
  useEffect(() => {
    if (!isEmpty(childRef) && show) {
      childRef.current.focus();
    }
  }, [show]);
  const { width } = useWindowSize();
  return (
    <div
      className="Tooltip-Wrapper"
      ref={ref}
    >
      {children}
      {show && (
        <div className={`Tooltip-Tip ${width > 900 ? 'right' : 'bottom'}`}>
          {content}
        </div>
      )}
    </div>
  );
};

Tooltip.propTypes = {
  children: PropTypes.node.isRequired,
  content: PropTypes.string,
  hide: PropTypes.func.isRequired,
  show: PropTypes.bool,
  childRef: PropTypes.instanceOf(Object),
};

Tooltip.defaultProps = {
  content: 'Hi',
  show: false,
  childRef: {},
};

We can simple use it like this.

  <Tooltip content="Wrong email or password" show={showError} hide={() => { this.setState({ showError: false }); }} childRef={this.emailRef}>
                <input type="text" name="email" value={email} placeholder="Email" required onChange={this.updateState} ref={this.emailRef} autoComplete="username email" />
              </Tooltip>
custom  tooltip component in React

Feel free to write your valuable comments. I hope you have learned something new today.

How to print pdf without opening the file in Reactjs

Bootstrap tooltip

Categories: Reactjs