Tooltip

Tooltip

Overview

A tooltip provides extra information about a form field, a link, a button, or other focusable element. It must be triggered by both focus and hover events and remains on the screen as long as the trigger has the focus. The focus does not move to the tooltip.



Turn on a screen reader to experience this example in action.

Tooltips on form inputs:

Tooltips on links:

Link 1

Link 2

Tooltips on buttons:

Initial HTML Markup

Tooltip for inputs:

<p>
  <label for="nameInput">First Name</label>
  <input id="nameInput" type="text" autocomplete="given-name" data-deque-tooltip="Your given name"
    class="deque-input">
</p>
<p>
  <label for="lastInput">Last Name</label>
  <input id="lastInput" type="text"
  autocomplete="family-name"
    data-deque-tooltip="Your surname or family name" class="deque-input">
</p>

Tooltip for links:

<p>
  <a id="demo-link1" href="javascript:void(0);"
    data-deque-tooltip="Tooltips can contain markup such as
    <strong>&amp;lt;strong&amp;gt;</strong> and <em>&amp;lt;em&amp;gt;</em>
    and even images:<br> <img src='assets/images/module-aria/sun.png'
    alt='Emoji smiling sun' height='24' width='24'>">
      Link 1
  </a>
</p>
<p>
  <a id="demo-link2" href="javascript:void(0);"
    data-deque-tooltip="This is non-functional demo
    link with a tooltip">
      Link 2
  </a>
</p>

Tooltip for buttons:

<p>
  <button id="demo-button1" class="deque-button"
    data-deque-tooltip="This particular tooltip is longer than the others,
    and spans several rows. The height is calculated automatically by the
    script and the offset is applied appropriately. It is possible to create
    long tooltips, but it is NOT recommended. The aria-describedby attribute
    does not allow screen reader users to pause in the middle of a tooltip
    and start again where they left off. They have to focus again on
    the element and listen to the whole tooltip from the beginning.">
      Button 1
  </button>
  <button id="demo-button2" class="deque-button"
    data-deque-tooltip="This is non-functional demo link with a tooltip">
      Button 2
  </button>
</p>
<p>
  <button id="demo-button3" class="deque-button" data-deque-tooltip="This is a non-functional demo button with a tooltip">Button 3</button>
</p>

JavaScript

Required: The complete JavaScript file (for all patterns in the library): deque-patterns.min.js

Key parts of the JavaScript file

Note: The code below is functional only in the context of the complete JavaScript file.

In the @tooltip section:


      function createTooltip(trigger) {
        var tipText = trigger.getAttribute('data-deque-tooltip');
        var tipID = (0, _guidUtils.generateGuid)();

        /* wrap the element (trigger) in a container */
        var tipWrapper = document.createElement('span');
        tipWrapper.classList.add('deque-tooltip-wrapper');
        trigger.parentNode.insertBefore(tipWrapper, trigger);
        /* move element into the wrapper */
        tipWrapper.appendChild(trigger);

        /* create tooltip */
        var tip = document.createElement('span');
        tip.setAttribute('role', 'tooltip');
        tip.id = tipID;
        tip.classList.add('deque-tooltip');

        tip.innerHTML = '<span aria-label="Tooltip :"></span>' + tipText;
        /* move tooltip into the wrapper */
        tipWrapper.appendChild(tip);

        /* carry over CSS values */
        var triggerStyle = window.getComputedStyle(trigger);
        var triggerCSS = [];
        triggerCSS['display'] = triggerStyle.getPropertyValue('display');

        function getTriggerPosition() {
          /* get element's size and position */
          var triggerPosition = [];
          triggerPosition['width'] = trigger.offsetWidth;
          triggerPosition['height'] = trigger.offsetHeight;
          return triggerPosition;
        }

        function getTooltipPosition(triggerPosition) {
          /* get/set positioning preferences */
          var triggerPositionPref = trigger.getAttribute('data-deque-tooltip-position');

          /* get tooltip dimensions */
          var tipStyle = window.getComputedStyle(tip);
          var tipCSS = [];
          tipCSS['width'] = tipStyle.getPropertyValue('width');
          tipCSS['width'] = tipCSS['width'].replace('px', '');
          tipCSS['width'] = Number(tipCSS['width']);
          tipCSS['height'] = tipStyle.getPropertyValue('height');
          tipCSS['height'] = tipCSS['height'].replace('px', '');
          tipCSS['height'] = Number(tipCSS['height']);

          /*triggerPositionPref is currently not being used in the code,
          but the idea is that developers could specify where the 
          tooltip should be placed: top, right, bottom, left, or top-right */
          if (triggerPositionPref == 'right') {
            tip.style.left = '5px';
            // put other calculations here
          }
          if (triggerPositionPref == 'top') {
            // put calculations here
          } else {
            var newLeft = triggerPosition['width'] - 10;
            //var newTop = tipCSS['height'] + 5;
            tip.style.left = newLeft - 10 + 'px';
            //tip.style.top = '-' + (newTop) + 'px';
            tip.style.bottom = '120%';
          }
        }

        (0, _noclobberUtils.noClobber)(trigger, 'aria-describedby', tipID);

        var causes = {
          INITIAL: 'initial',
          FOCUS: 'focus',
          MOUSE: 'mouse',
          ESCAPE: 'escape'
        };

        var currentCause = causes.INITIAL;

        function showTip(cause) {
          if (currentCause === causes.FOCUS) {
            // prevents 'MOUSE' cause from replacing 'FOCUS' cause.
            return;
          }

          tip.classList.remove('deque-hidden');
          tip.setAttribute('aria-hidden', 'false');

          currentCause = cause;

          var p = getTriggerPosition();
          getTooltipPosition(p);
          /* For some reason, the position is incorrectly 
          calculated on inline elements the first time the
          getTooltipPosition function runs, so it has to run
          twice. Possible bug in the script's logic? */
          getTooltipPosition(p);
        }

        function hideTip(cause) {
          if (cause === causes.ESCAPE || currentCause === cause) {
            tip.classList.add('deque-hidden');
            tip.setAttribute('aria-hidden', 'true');
            currentCause = '';
          }
        }

        function escapeHandler() {
          hideTip(causes.ESCAPE);
        }

        var cancelElementEscape = (0, _keyboardUtils.onElementEscape)(document.body, escapeHandler);

        var focusHandler = showTip.bind(null, causes.FOCUS);
        var blurHandler = hideTip.bind(null, causes.FOCUS);
        var overHandler = showTip.bind(null, causes.MOUSE);
        var outHandler = hideTip.bind(null, causes.MOUSE);

        trigger.addEventListener('focus', focusHandler);
        trigger.addEventListener('blur', blurHandler);
        trigger.addEventListener('mouseover', overHandler);
        trigger.addEventListener('mouseout', outHandler);

        /*// eslint-disable-next-line no-console
        console.log(trigger);
        // eslint-disable-next-line no-console
        console.log(trigger.parentNode.querySelector('.deque-tooltip'));*/

        if (trigger.parentNode.querySelector('.deque-tooltip')) {
          trigger.parentNode.querySelector('.deque-tooltip').addEventListener('mouseover', overHandler);
          trigger.parentNode.querySelector('.deque-tooltip').addEventListener('mouseout', outHandler);
        }

        hideTip(causes.INITIAL);

        return function disableTooltip() {
          hideTip(causes.ESCAPE);

          cancelElementEscape();

          try {
            tip.remove();
          } catch (err) {
            /*no op*/
          }

          trigger.removeEventListener('focus', focusHandler);
          trigger.removeEventListener('blur', blurHandler);
          trigger.removeEventListener('mouseover', overHandler);
          trigger.removeEventListener('mouseout', outHandler);
          trigger.removeAttribute('aria-describedby');
        };
      }

      function initializeAllTooltips() {
        var itemsToTip = document.querySelectorAll('[data-deque-tooltip]');
        for (var i = 0; i < itemsToTip.length; i++) {
          createTooltip(itemsToTip[i]);
        }
      }

      initializeAllTooltips();
      

In the @guidUtils section:


      /*
        note - not a true guid. I prepend 'g' because 
        the ID of an element cannot start with a numeral
      */

      function generateGuid() {
        var S4 = function S4() {
          return ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1);
        };
        return 'g' + (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
      }
      

In the @keyboardUtils section:


      var KEYS = exports.KEYS = {
        BACKSPACE: 8,
        TAB: 9,
        ENTER: 13,
        SHIFT: 16,
        CTRL: 17,
        ALT: 18,
        ESCAPE: 27,
        SPACE: 32,
        LEFT: 37,
        RIGHT: 39,
        UP: 38,
        DOWN: 40,
        F10: 121,
        HOME: 36,
        END: 35,
        PAGE_UP: 33,
        PAGE_DOWN: 34
      };

      function bindElementToEventValue(element, eventName, testValue, handler) {
        function localHandler(e) {
          if (e.which === testValue) {
            handler(e);
          }
        }

        element.addEventListener(eventName, localHandler);
        return function () {
          element.removeEventListener(eventName, localHandler);
        };
      }

      function bindElementToKeypressValue(element, testValue, handler) {
        return bindElementToEventValue(element, 'keypress', testValue, handler);
      }

      function bindElementToKeydownValue(element, testValue, handler) {
        return bindElementToEventValue(element, 'keydown', testValue, handler);
      }

      function onElementEnter(element, handler) {
        return bindElementToKeydownValue(element, KEYS.ENTER, handler);
      }

      function onElementEscape(element, handler) {
        return bindElementToKeydownValue(element, KEYS.ESCAPE, handler);
      }

      function onElementSpace(element, handler) {
        return bindElementToKeypressValue(element, KEYS.SPACE, handler);
      }

      function onElementLeft(element, handler) {
        return bindElementToKeydownValue(element, KEYS.LEFT, handler);
      }

      function onElementRight(element, handler) {
        return bindElementToKeydownValue(element, KEYS.RIGHT, handler);
      }

      function onElementUp(element, handler) {
        return bindElementToKeydownValue(element, KEYS.UP, handler);
      }

      function onElementDown(element, handler) {
        return bindElementToKeydownValue(element, KEYS.DOWN, handler);
      }

      function onElementHome(element, handler) {
        return bindElementToKeydownValue(element, KEYS.HOME, handler);
      }

      function onElementEnd(element, handler) {
        return bindElementToKeydownValue(element, KEYS.END, handler);
      }

      function onElementPageUp(element, handler) {
        return bindElementToKeydownValue(element, KEYS.PAGE_UP, handler);
      }

      function onElementPageDown(element, handler) {
        return bindElementToKeydownValue(element, KEYS.PAGE_DOWN, handler);
      }

      function onElementF10(element, handler) {
        return bindElementToKeydownValue(element, KEYS.F10, handler);
      }

      function isAlphaNumeric(charCode) {
        return charCode >= 48 && charCode <= 90 /* numbers, uppercase letters */
          || charCode >= 97 && charCode <= 122 /* lowercase letters */;
      }

      function onElementCharacter(element, handler) {
        function localHandler(e) {
          var charCode = e.which;
          if (isAlphaNumeric(charCode)) {
            handler(e);
          }
        }

        element.addEventListener('keypress', localHandler);
        return function () {
          element.removeEventListener('keypress', localHandler);
        };
      }

      function trapEnter(element) {
        onElementEnter(element, function (e) {
          e.stopPropagation();
          e.preventDefault();
        });
      }
      

In the @noclobberUtils section:



      /* Whitespace RegExp. */

      var whitespace = /\s+/;

      /**
       * Add `attr=val` without clobbering an existing value.
       * @param  {Element}  element [Element object]
       * @param  {String}   attr    [Attribute to update]
       * @param  {Array}    ids     [rest parameters of ids to add]
       */
      function noClobber(element, attr) {
        var current = element.getAttribute(attr);
        // if there is no value, just add the new one

        for (var _len = arguments.length, ids = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
          ids[_key - 2] = arguments[_key];
        }

        if (!current) {
          return element.setAttribute(attr, ids.join(' '));
        }

        // remove "extra" whitespace, then split by any whitespace
        var parts = current.trim().split(whitespace);

        ids.map(function (val) {
          if (!~parts.indexOf(val)) {
            parts.push(val);
          }
        });

        // set new val
        return element.setAttribute(attr, parts.join(' '));
      }

      

Note: No initialization code is necessary. All functionality is contained in the external JavaScript file.

CSS

Required: The complete CSS file (for all patterns in the library): deque-patterns.min.css

Key styles within the CSS file (other styles may also be necessary):


.deque-tooltip-wrapper {
  position: relative;
}
.deque-tooltip {
  box-sizing: border-box;
  font-size: 13px;
  position: absolute;
  background: #ffffff;
  border: 1px solid rgba(0, 0, 0, 0.3);
  min-width: 104px;
  max-width: 250px;
  margin-right: -340px;
  padding: 8px 6px;
  line-height: 16px;
  z-index: 700;
}
.deque-tooltip:after {
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-top: 10px solid #b2b2b2;
  top: 100%;
  position: absolute;
  bottom: 0;
  height: 0;
  width: 0;
  left: -1%;
  content: "";
}

Implementation Instructions

Step 1: Add Dependencies

Add deque-patterns.min.css in the <head> of the document.

<link rel="stylesheet" type="text/css" href="deque-patterns.min.css">

Add a script link to deque-patterns.min.js to the bottom of the page.

<script type="text/javascript" src="deque-patterns.min.js"></script>

Step 2: Add HTML

  • Create a container with data-deque-tooltip="" with whatever you want the tooltip to display. The javascript will automatically initialize all tooltips with this attribute.