Skip to content

Instantly share code, notes, and snippets.

@bojidaryovchev
Created April 26, 2023 19:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bojidaryovchev/1dfe2913066f135c274bf18a00724d80 to your computer and use it in GitHub Desktop.
Save bojidaryovchev/1dfe2913066f135c274bf18a00724d80 to your computer and use it in GitHub Desktop.
React HoverPopup - a popup that moves with the mouse

Hover Popup Component

This is a React component that displays a hover popup when a user hovers over a specified element.

Installation

To use this component, simply import it into your project:

import HoverPopupComponent from './path/to/HoverPopupComponent';

Usage

Create a ref for the element you want to trigger the hover popup:

const hoverElementRef = useRef<HTMLDivElement>(null);

Attach the ref to the element that will trigger the hover popup:

<div ref={hoverElementRef}>Hover over me!</div>

Use the HoverPopupComponent and pass the hoverElementRef as a prop:

<HoverPopupComponent hoverElementRef={hoverElementRef}>
  <p>Your custom content goes here!</p>
</HoverPopupComponent>

Now, when the user hovers over the specified element, the hover popup will be displayed with your custom content.

import classNames from 'classnames';
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
export interface HoverPopupComponentProps {
hoverElementRef?: React.RefObject<HTMLElement>;
}
const HoverPopupComponent: React.FC<PropsWithChildren<HoverPopupComponentProps>> = ({ hoverElementRef, children }) => {
const [mouseEvent, setMouseEvent] = useState<MouseEvent>();
useEffect(() => {
const onMouseEvent = (mouseEvent: MouseEvent): void => {
setMouseEvent(mouseEvent);
};
window.addEventListener('mousemove', onMouseEvent);
return () => {
window.removeEventListener('mousemove', onMouseEvent);
};
}, []);
const hoverPopupRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!hoverPopupRef.current || !mouseEvent) {
return;
}
const delta = 12;
const popup = hoverPopupRef.current;
const popupWidth = popup.offsetWidth;
const popupHeight = popup.offsetHeight;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = mouseEvent.clientX + delta;
let top = mouseEvent.clientY + delta;
if (left + popupWidth > viewportWidth) {
left -= popupWidth + 2 * delta;
}
if (top + popupHeight > viewportHeight) {
top -= popupHeight + 2 * delta;
}
hoverPopupRef.current.style.setProperty('left', `${left}px`);
hoverPopupRef.current.style.setProperty('top', `${top}px`);
}, [hoverPopupRef, mouseEvent]);
const [hoverElementActive, setHoverElementActive] = useState<boolean>();
const [fadeOutOngoing, setFadeOutOngoing] = useState<boolean>();
useEffect(() => {
if (!hoverElementRef?.current) {
return;
}
let timeout: NodeJS.Timeout | undefined;
const mouseEnterHandler = (): void => {
setHoverElementActive(true);
clearTimeout(timeout);
setFadeOutOngoing(false);
};
const fadeOutDuration = 300;
const mouseLeaveHandler = (): void => {
setFadeOutOngoing(true);
timeout = setTimeout(() => {
setFadeOutOngoing(false);
setHoverElementActive(false);
}, fadeOutDuration);
};
const hoverElement = hoverElementRef.current;
hoverElement.addEventListener('mouseenter', mouseEnterHandler);
hoverElement.addEventListener('mouseleave', mouseLeaveHandler);
return () => {
hoverElement.removeEventListener('mouseenter', mouseEnterHandler);
hoverElement.removeEventListener('mouseleave', mouseLeaveHandler);
};
}, [hoverElementRef]);
return createPortal(
<>
{hoverElementActive && (
<div
ref={hoverPopupRef}
className={classNames('fixed p-2 z-10 animate-fadeIn', {
'animate-fadeOut': fadeOutOngoing,
})}
>
{children}
</div>
)}
</>,
document.body
);
};
export default HoverPopupComponent;
export { default as HoverPopupComponent } from './HoverPopup';
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
keyframes: {
fadeIn: {
'0%': {
opacity: '0',
},
'100%': {
opacity: '1',
},
},
fadeOut: {
'0%': {
opacity: '1',
},
'100%': {
opacity: '0',
},
},
},
animation: {
fadeIn: 'fadeIn 0.3s forwards',
fadeOut: 'fadeOut 0.3s forwards',
},
},
},
variants: {
extend: {
animation: ['responsive'],
},
},
plugins: [
function ({ addUtilities }) {
const newUtilities = {
'.animate-fadeIn-500': {
animation: 'fadeIn 0.5s forwards',
},
'.animate-fadeIn-1000': {
animation: 'fadeIn 1s forwards',
},
'.animate-fadeOut-500': {
animation: 'fadeOut 0.5s forwards',
},
'.animate-fadeOut-1000': {
animation: 'fadeOut 1s forwards',
},
};
addUtilities(newUtilities);
},
],
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment