Tooltips are trickier than they look; For the optimal experience, you have to handle many intricacies and edge cases.
Positioning is complex, with its many edge cases (screen edges, scrolling). Hover behavior could be more intricate, so the tooltip remains open while you hover over it, and not just its target. Appearance and disappearance should both have delays and maybe animation. AND you don't want multiple tooltips appearing together and hiding each other.
Today I'll write about this last part. Let me know if you'd like me to elaborate on the other parts :)
===
Who am I?
I'm Miki Stanger, a front-end architect and advisor. I've been to the industry for 12 years. I currently accompany companies and help with architecture, decisions, mentoring and problem-solving.
I'm always looking for cool people and projects to help with! You can read more about me and find my up-to-date contact information here: https://about.me/mikistanger/
===
Tooltips (And When To Use Them)
Tooltips are a way to show additional information about something. They are usually triggered by hovering over an element, and should show an explanation about how to read or use a part of your system.
Because it is sensitive to mouse movement and positioned next to other elements, it is best used for short texts; Large bodies of text will require the user to not touch their mouse for a while (even though some people move the mouse, click or select texts while reading or while multitasking), and might create a huge box that overshadows or even hides the parts it explains.
If your explanation is more than a few sentences long, you could use a modal instead; A modal is more persistent and allows the use of photos and videos, and are thus a better medium for larger amounts of content.
The Problem
The premise of a tooltip is supposedly basic - Hover over something and some element should appear in the right position. However, if you have several available tooltips next to each others, this could happen:
See how the tooltips hide each other? This kind of clutter not only looks messy, it could also steal the user's focus from the one tooltip they're looking for.
The Solution
When we show a tooltip, we could simply hide the other ones.
As with a lot of tooltip-related things, this is a bit more complex than it sounds, especially when working with React. There are two approaches here:
- Have a single tooltip component and control it through props. You could pass the method through a store or using context, so they're available globally. This is not a bad way, except that it might unnecessarily trigger a full re-render. We'll look at a different approach that doesn't require all of those additional mechanisms.
- Have multiple tooltip instances that live where they are used, and some way to turn the others off. This is also achievable with a store or a context, but there's a better way (imo) to do it - manage hiding tooltips with a singleton.
What Is A Singleton?
A singleton is a single instance of an object that's exposed to the whole system (as opposed to creating multiple objects or instances of a class).
A singleton is useful when one needs to guarantee the same options and methods will be available and persistent throughout the project. A common example for a singleton is a logging instance - a single instance of a class, with its configuration and options, which guarantee that all logging will be done with the same options and policies everywhere.
The PreventMultipleTooltipsManager
Singleton
Our tooltip is a component that wraps another component and surrounds it with a tooltip-triggering component:
export const WithTooltip({ children }) => {
const [isVisible, setIsVisible] = useState(false);
const setIsVisibleToFalse = useCallback(() => {
setIsVisible(false);
}, [setIsVisible]);
const setIsVisibleToTrue = useCallback(() => {
setIsVisible(true);
}, [setIsVisible]);
// Rest of logic
return <TooltipWithStylesAndStuff
onMouseEnter={setIsVisibleToTrue}
onMouseLeave={setIsVisibleToFalse}
>
{children}
</TooltipWithStylesAndStuff>
}
OUTSIDE of the component, we'll create a class and initiate a single instance of it - a singleton:
class PreventMultipleTooltipsManager {}
const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
This singleton is going to hold a function that hides the currently visible tooltip (its setIsVisibleToFalse
function). When a new tooltip is shown, the singleton will allow us to use the now-previous tooltip's function and replace it with the current one's. We can do both operations in a single method, as they're always going to be called together in the same order:
class PreventMultipleTooltipsManager {
private currentHideFunc: (() => void) | null = null;
public changeTooltip(hideFunc) {
this.currentHideFunc?.();
this.currentHideFunc = hideFunc;
}
}
const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
We're also going to add an unmount
method. This method will be used in a tooltip's teardown, in case it is removed from the DOM while visible. We'll later use it in a teardown function that's the return value of a useEffect
:
class PreventMultipleTooltipsManager {
private currentHideFunc: (() => void) | null = null;
public changeTooltip(hideFunc) {
this.currentHideFunc?.();
this.currentHideFunc = hideFunc;
}
public unmount(hideFunc) {
if (hideFunc === this.currentHideFunc) {
this.currentHideFunc = null;
}
}
}
const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
The singleton is done. Now we can call it inside the component. We'll use its changeTooltip
method in isSetVisibleToTrue
, and its unmount
method in a useEffect
:
export const WithTooltip({ children }) => {
const [isVisible, setIsVisible] = useState(false);
const setIsVisibleToFalse = useCallback(() => {
setIsVisible(false);
}, [setIsVisible]);
const setIsVisibleToTrue = useCallback(() => {
preventMultipleTooltipsManager.changeTooltip(setIsVisibleToFalse);
setIsVisible(true);
}, [setIsVisible, setIsVisibleToFalse]);
useEffect(() => (
() => {
preventMultipleTooltipsManager.unmount(setIsVisibleToFalse);
}
), [setIsVisibleToFalse])
// Rest of logic
return <TooltipWithStylesAndStuff
onMouseEnter={setIsVisibleToTrue}
onMouseLeave={setIsVisibleToFalse}
>
{children}
</TooltipWithStylesAndStuff>
}
And that's it! Look at it behaving nicely:
Conclusion
Tooltips are a wonderful case of how thinking about the small details makes a difference between buggy, unfriendly component and a delightful one. Today, we dug into one of several such details, learned what a singleton is, and use this knowledge to make our tooltips better.
What do you think? How would you implement a solution to this same problem?
Top comments (1)
You could put this behaviour into a hook: