<div id="root"></div>
@import url('https://fonts.googleapis.com/css?family=Encode+Sans+Expanded');

html,body {
  font-family: 'Encode Sans Expanded', sans-serif; 
  height: 100%;
  margin: 1rem;
  margin-top: 25vh;
}

u.tip {
  text-decoration: underline;
  text-decoration-style: dashed;
  text-decoration-color: #69C;
  cursor: default;
}
.tooltip {
  display: inline-block;
  background-color: #444;
  color: #fff;
  border-radius: 6px;
  padding: .25rem .5rem;
  margin-top: 10px;
  font-size: .75rem;
  cursor: default;
  
  &::after {
    content: '';
    position: absolute;
    top: -5px;
    left: 50%;
    transform: translateX(-50%) rotate(45deg);
    background-color: #444;
    width: 10px;
    height: 10px;
  }
}
View Compiled
const { Component } = React;
const { render } = ReactDOM;
const cx = classNames;

// const useOutsideClick = (ref, callback) => {
//   const handleClick = e => {
//     if (ref.current && !ref.current.contains(e.target)) {
//       callback();
//     }
//   };

//   useEffect(() => {
//     document.addEventListener("click", handleClick);

//     return () => {
//       document.removeEventListener("click", handleClick);
//     };
//   });
// };

const composeCallbacks = (...callbacks) => {
  return (...args) => {
    const fns = callbacks.filter(Boolean);
    for (const callback of fns) callback(...args);
  };
};

const mergeRefs = (...refs) => {
  const filteredRefs = refs.filter(Boolean);
  if (!filteredRefs.length) return null;
  if (filteredRefs.length === 0) return filteredRefs[0];
  return inst => {
    for (const ref of filteredRefs) {
      if (typeof ref === 'function') {
        ref(inst);
      } else if (ref) {
        ref.current = inst;
      }
    }
  };
};

const Tooltip = ({ text, children }) => {
  const [active, setActive] = React.useState(false);
  const [{ top, left }, setPosition] = React.useState({ top: 0, left: 0 });
  const trigger = React.useRef(null);
  const tip = React.useRef(null);
  
  const show = () => setActive(true);
  const hide = () => setActive(false);
  
  const child = typeof children === 'string' ? 
        <span>{children}</span> :
        React.Children.only(children);
  
  React.useEffect(() => {
    if (active && (trigger && trigger.current) && (tip && tip.current)) {
      const triggerEl = trigger.current.getBoundingClientRect();
      const tipEl = tip.current.getBoundingClientRect();
      setPosition({ 
        top: (triggerEl.y + window.pageYOffset) + triggerEl.height, 
        left: (triggerEl.x + window.pageXOffset) + ((triggerEl.width - tipEl.width)/2) 
      });
    }
  }, [active, trigger, tip]);
  
  return (
    <>
    { React.cloneElement(child, {
        ...child.props,
        ref: mergeRefs(child.ref, trigger),
        onMouseEnter: child.props['onMouseEnter'] ? 
          composeCallbacks(child.props.onMouseEnter, show) : 
          show,
        onMouseLeave: child.props['onMouseLeave'] ? 
          composeCallbacks(child.props.onMouseLeave, hide) : 
          hide
      })}  
    { active && ReactDOM.createPortal(
        <div className="tooltip" ref={tip} style={{ position: 'absolute', top, left }}>
          {text}
        </div>,
        document.body)
    }
    </>
  );
}

const App = () => {  
  return (
    <div>
      A tooltip{" "}
      <Tooltip text="a tooltip">
        <u className="tip">hover me</u>
      </Tooltip>
      {" "}
      within a sentence.
    </div>
  );
};


render(
  <App/>
, document.querySelector('#root'))
View Compiled
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css

External JavaScript

  1. https://unpkg.com/react@16/umd/react.development.js
  2. https://unpkg.com/react-dom@16/umd/react-dom.development.js
  3. https://unpkg.com/prop-types/prop-types.js
  4. https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.min.js
  6. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js