JSFiddle - React, Tailwind, and code Playground

HTML

<span title="This is a tooltip to the right" data-tooltip="right">Hover over me for a tooltip to the right</span><br>

CSS

span{
  background:#000;
  padding:10px;
  color:#fff;
}

.tooltip {
  position: absolute;
  background:#f0f; /*Change this color to see the triangle change color too */
  color:#fff;
  padding: 5px;
  z-index: 1000;
  max-width: 200px;
  transition: opacity .2s ease;
  opacity: 0;
  box-sizing: border-box;
  border-radius: 5px;
  pointer-events: none;
}
.tooltip:after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  background: inherit;
  opacity: 0;
  transition: opacity .2s ease;
  z-index: -1;
}
.tooltip.fade {
  opacity: 1;
}
.tooltip.fade:after {
  opacity: 1;
}
.tooltip.right:after {
  top: 50%;
  left: 0;
  transform: translate(-50%, -50%) rotate(45deg);
}

JavaScript

const parent = document.querySelector("span");

const tip = document.createElement("div");
tip.textContent = parent.title;
tip.classList.add("tooltip");

parent.addEventListener("mouseenter", event => {
	/*Add the tooltip to the DOM, and then place it where it needs to go*/
  addTooltip(parent, tip);
  positionTip(parent, tip);
});
parent.addEventListener("mouseleave", event => {
	//remove it when it's no longer needed
  removeTooltip(parent, tip);
});


function addTooltip(parent, tip) {
	/*prevent the default tooltip behavior from showing by temporarily emptying the title attribute*/
  parent.dataset.title = parent.title;
  parent.title = "";
	
	/*getting rid of the previous timeouts before adding a new one prevents strange flickering and 
  incorrect states*/
  if (parent.toOut !== null) clearTimeout(parent.toOut);
  
  /*after 500 ms has passed, we will fade the tooltip in*/
  parent.appendChild(tip);
  parent.toIn = setTimeout(() => {
    tip.classList.add("fade");
    parent.toIn = null;
  }, 500);
}
function removeTooltip(parent, tip) {
  /*restore the original title attribute data*/
  parent.title = parent.dataset.title;
  
  /*getting rid of the previous timeouts before adding a new one prevents strange flickering and 
  incorrect states*/
  if (parent.toIn !== null) clearTimeout(parent.toIn);
  
  /*fade the tooltip out and remove it after the transition is complete*/
  tip.classList.remove("fade");
  parent.toOut = setTimeout(() => {
    parent.removeChild(tip);
    parent.toOut = null;
  }, 500);
}

function positionTip(parent, tip) {
  const orientation = parent.dataset.tooltip;
  const padding = 10;
  
  /*use CSS styles to position the tooltip, dynamically determined by the height and width of the tooltip
  and the item it's attached to*/
  tip.style.left = `${parent.offsetLeft +
    parent.offsetWidth +
    padding}px`;
  tip.style.top = `${parent.offsetTop +
    parent.offsetHeight / 2 -
    tip.offsetHeight / 2}px`;
  tip.classList.add("right");
}