Skip to content

Instantly share code, notes, and snippets.

@deanebarker
Last active July 26, 2024 20:32
Show Gist options
  • Save deanebarker/8a205c360386661724c7ab62ef754e65 to your computer and use it in GitHub Desktop.
Save deanebarker/8a205c360386661724c7ab62ef754e65 to your computer and use it in GitHub Desktop.
Simple code to enable mouseover tooltips

This is the simplest form of a mouseover "tooltip" UI that I could come up with.

You can see it used on local hyperlinks at https://deanebarker.net/.

Note: this is NOT a "library." This is starter code. There are no settings or configuration (meaning: everything is hard-coded) nor has it been designed to be extensible -- you are expected copy this code and change it. It's essentially just an example.

To use

  1. Call bindTips, passing a collection of elements.
  2. Implement your own getTipContent. It will be passed the entire element that triggered the tip. Do whatever you want (read from an attribute, do a lookup on the HREF, whatever). Return the HTML content for the tip.

Example:

bindTips(document.querySelectorAll('a'));

function getTipContent(el) {
  var href = el.getAttribute('href');
  return href + " is a great URL!!!";
}

But I don't want to show tooltips on [insert criteria here]...

You control the collection passed to bindTips. Form that collection however you want.

But I want them to look different...

There is literally one element you need to style. The CSS is right there. Go nuts.

But I just want to display the content of an attribute...

getTipContent is passed the entire element. Do whatever you want.

<a href="whatever" data-tooltip="This is a really cool website"/>
function getTipContent(el) {
  return el.getAttribute('data-tooltip');
}

But I want to retieve the tooltip from [insert hare-brained idea here]...

Again, do whatever you want in getTipContent. It's designed to show some placeholder text until that function returns. I wouldn't dilly-dally or anything, but reasonable I/O isn't going to look weird (I do a background HTTP call in my use case; it feels fine to me).

Why don't you have a settings/config object?

Again, this is not a plugin/module/extension/library. I wrote this for me and my one and only use case. There will never be an automated update to this code via npm or yarn or whatever.

Copy the code, do whatever you want without fear.

div.tip
{
padding: 1em;
border: solid 1px rgb(200,200,200);
background: rgb(240,240,240);
}
function bindTips(elements) {
elements.forEach(el => {
el.addEventListener("mouseenter", openTip);
el.addEventListener("mouseleave", closeTip);
});
}
async function openTip(e) {
// We're creating a DIV out of thin air, and we're going to tack it on to the end of the BODY
var div = document.createElement("div");
div.classList.add("tip")
div.style.position = "fixed";
// NOTE: You have to offset the tip here
// If you try to show the tip so it overlays the element, you'll have problems
// As it comes in, it will cause the mouseenter and mouseleave elements to fire repeatedly
// So you have to move it off the element (I use 20px in each direction below, YMMV)
div.style.top = (e.y+20) + "px";
div.style.left = (e.x+20) + "px";
// The tip appears with "Loading..." before that content is replaced with the return value of getTipContent.
// In my use case, I was doing an HTTP call to get the content, so there was too much lag.
// If you don't do any IO, the user should never see "Loading..."
// Alternatively, just move up the call to getTipContent and skip this whole thing...
div.innerHTML = "Loading...";
document.body.appendChild(div); // This shows it
// Now load the actual content...
div.innerHTML = await getTipContent(e.target);
}
function closeTip(e) {
// There should only ever be one on the page at any time
// But, just in case...
Array.from(document.querySelectorAll("div.tip")).forEach(el => {
document.body.removeChild(el);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment