Tooltips

Demo

Text about tooltips.

<button class="notifications" aria-labelledby="tooltip-label">  
  <!-- Your SVG, IMG, icon here! -->
</button>  
<div class="arrow_box" role="tooltip" id="tooltip-label">Hi, I'm tooltip text! Hopefully, something useful and brief.</div>
 
[role="tooltip"] {
  display: none;
  border: 2px solid black;
  padding: 10px;
  border-radius: 5px;
  width: 40%;
}
.arrow_box {
    position: relative;
    background: #fff;
    border: 2px solid #000;
    margin-top: 15px;
}
.arrow_box:after, .arrow_box:before {
    bottom: 100%;
    left: 11%;
    border: solid transparent;
    content: "";
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
}

.arrow_box:after {
    border-color: rgba(255, 255, 255, 0);
    border-bottom-color: #fff;
    border-width: 20px;
    margin-left: -20px;
}
.arrow_box:before {
    border-color: rgba(0, 0, 0, 0);
    border-bottom-color: #000;
    border-width: 23px;
    margin-left: -23px;
}

button:hover + [role="tooltip"],  
button:focus + [role="tooltip"] {  
  display: block;
}
button {
    font-size: 1.25rem;
    border-radius: 0.33em;
    font-family: inherit;
    width: 120px;
    height: 120px;
    color: #fefefe;
    padding: 0.75rem;
    border: 0;
    background: #fff;
}
svg {
    width: 100%;
} 
				
					
// use an anonymous funciton to encapsulate
(() => {
    // max width in pixels for tooltip
    const maxWidth = 500;

    main();

    function main() {
        let revealNameTips = document.querySelectorAll('[data-revealed-name]');

        // add event listeners
        for (const revealNameElement of revealNameTips) {
            prepareHoverableContentState(revealNameElement);
        }

        // remove content on escape or control
        document.body.addEventListener('keydown', toggleHoverContent);

        // handle viewport changes
        window.addEventListener('resize', moveTooltip);
    }

    /**
     * Adds event listeners that set the state of the component
     * @param {HTMLElement} element the element to add event listeners to
     */
    function prepareHoverableContentState(element) {
        // hover
        element.addEventListener('mouseenter', (e) => setState(element, true));
        element.addEventListener('mouseleave', (e) => setState(element, false));
        // focus
        element.addEventListener('focusin', (e) => setState(element, true));
        element.addEventListener('blur', (e) => setState(element, false));
    }

    /**
     * Sets the state and any state related items/attributes.
     * @param {HTMLElement} element the element that owns the state
     * @param {Boolean} isVisible true if visible
     */
    function setState(element, isVisible) {
        // typically, when content appears due to user interaction
        // we want to use ARIA-EXPANDED to convey the expanded or collapsed
        // state of this component. However, in this case, there is no new
        // information being presented that AT would not already know (the 
        // text being shown is the accessible name, or the accessible description).
        // As such, we forgo adding the ARIA-EXPANDED attribute as it would
        // likely only confuse screen reader users, as they might expect the
        // component to reveal new information on activation, rather than
        // perform an action.
        if (isVisible) {
            element.classList.remove('hidden');
        }
        else {
            element.classList.add('hidden');
        }
        repositionTooltip(element.querySelector('[data-visual-name]'));
    }

    /**
     * Finds all elements that are currently showing their tooltip,
     * and then hides those tooltips.
     * @param {KeyboardEvent} e a keyboard event
     */
    function toggleHoverContent(e) {
        // we return if the key pressed isn't Escape or Control
        if (e.key !== 'Escape' && e.key !== 'Control') return;
        // we get all currently visible content. We use querySelectorAll
        // in case the user has both focused an element and hovered a
        // different element.
        let hoveredElements = document.querySelectorAll('[data-revealed-name]:not(.hidden)');
        for (const hoveredElement of hoveredElements) {
            hoveredElement.classList.add('hidden');
        }
    }

    /**
     * Finds all currently visible tooltip text elements, and then
     * repositions them to ensure that they are not offscreen.
     */
    function moveTooltip() {
        // get all visible tooltips
        let visibleTips = document.querySelectorAll(
            '[data-revealed-name]:not(.hidden) [data-visual-name]'
        );
        for (const visibleTip of visibleTips) {
            repositionTooltip(visibleTip);
        }
    }

    /**
     * Checks the position of the tooltip text and repositions and resizes
     * the tooltipText element such that it remains within the viewport.
     * Important for ensuring that there is no loss of content when the viewport
     * is zoomed in.
     * @param {HTMLElement} tooltipText The tooltip text element that is shown on hover/focus
     */
    function repositionTooltip(tooltipText) {
        // reset the styling
        tooltipText.style.removeProperty('width');
        tooltipText.style.removeProperty('left');
        tooltipText.style.removeProperty('white-space');
        // get tooltipText dimensions
        let tooltipRect = tooltipText.getBoundingClientRect();
        // set max-width
        if (tooltipRect.width > maxWidth) {
            tooltipText.style.width = maxWidth + 'px';
            tooltipText.style.whiteSpace = 'normal';
        }
        tooltipRect = tooltipText.getBoundingClientRect();
        // get parentElement dimensions (should be a [data-revealed-name] element)
        let parentRect = tooltipText.parentElement.getBoundingClientRect();
        // viewport width minus body margin
        let vw = document.body.clientWidth + 2 * getPropertyAsNumber(document.body, 'margin');
        // get the padding of the tooltip
        let paddingLeft = getPropertyAsNumber(tooltipText, 'padding-left');
        // math to move the tooltip to the middle
        let parentMiddlePoint = parentRect.x + (parentRect.width / 2);
        let tooltipStartPoint = parentMiddlePoint - (tooltipRect.width / 2);
        let tooltipLeftOffset = tooltipStartPoint - parentRect.x;
        tooltipText.style.left = tooltipLeftOffset + 'px';
        
        // get new tooltip dimensions
        tooltipRect = tooltipText.getBoundingClientRect();
        // if tooltipRect width is larger than viewport, set to viewport;
        if (tooltipRect.width > vw) {
            let widthOffset = (vw - (2 * paddingLeft));
            let leftOffset = (-1 * parentRect.x) +  paddingLeft;
            tooltipText.style.width = widthOffset + 'px';
            tooltipText.style.left = leftOffset + 'px';
            // in the style sheet we've set the "white-space" CSS property
            // to nowrap, but when the text is longer than the width of the
            // viewport, we need to wrap the text
            tooltipText.style.whiteSpace = 'normal';
        }
        // if it starts off screen to the left
        else if (tooltipRect.x < 0) {
            let offset = (-1 * parentRect.x) + paddingLeft;
            tooltipText.style.left = offset + 'px';
        }
        // if it is off screen to the right
        else if ((tooltipRect.x + tooltipRect.width) > vw) {
            let offset = (-1 * (parentRect.x + tooltipRect.width - vw + paddingLeft));
            tooltipText.style.left = offset + 'px';
        }
    }

    /**
     * Gets the value of a CSS property for an element as a number. 
     * @param {HTMLElement} element gets the CSS property of this element
     * @param {String} property the CSS property name
     * @returns the CSS property's value as a Number (note CSS values are typically in pixels)
     */
    function getPropertyAsNumber(element, property) {
        return Number(
            window
                .getComputedStyle(element)
                .getPropertyValue(property)
                .match(/\d+/gi)
        );
    }
})();
				
			

Why use a tooltip component?

Tooltip components in web development are used to provide additional information or context when users interact with specific elements on a webpage. These components typically display a small pop-up box containing text or multimedia content when users hover over or focus on an element, such as a button or link. From a digital accessibility perspective, tooltips play a crucial role in enhancing user experience for individuals with disabilities by offering supplementary information that may not be immediately apparent from the element’s visible content. This feature can benefit users who rely on screen readers or keyboard navigation, as tooltips provide concise explanations or instructions for interactive elements, improving comprehension and navigation on the webpage.

What are some accessibility concerns?

  1. Lack of Keyboard Accessibility
    • Concern: Without proper keyboard accessibility, users who rely on keyboard navigation cannot interact with tooltips, making the content inaccessible to them.
    • Mitigation: Ensure tooltips are keyboard accessible by making them focusable with the tabindex attribute and allowing users to activate them using the Enter or Space key. Implement ARIA roles and attributes such as role=”tooltip” and aria-describedby to associate tooltips with their triggering elements and announce them to screen reader users.
  2. Insufficient Contrast
    • Concern:ย Low contrast between the tooltip text and background color can make it difficult for users with low vision or color blindness to read the content.
    • Mitigation: Ensure sufficient color contrast between the tooltip text and background according to WCAG guidelines (minimum contrast ratio of 4.5:1 for normal text or 3:1 for large text). Use high-contrast color schemes or provide an option for users to customize tooltip colors to meet their visibility needs.
  3. Inconsistent or Unpredictable Behavior
    • Concern: Inconsistent behavior of tooltips, such as appearing and disappearing unpredictably, can confuse users and disrupt their workflow.
    • Mitigation:ย Ensure tooltips have consistent behavior across different elements and interactions throughout the website. Implement a reasonable delay before displaying tooltips to prevent them from appearing too quickly and causing distraction, and ensure they remain visible for an adequate duration before disappearing.
  4. Lack of Screen Reader Compatibility
    • Concern:ย Inaccessible tooltips that are not announced or properly described by screen readers can exclude users with visual impairments from accessing the additional information.
    • Mitigation:ย Associate tooltips with their triggering elements using ARIA attributes to ensure they are announced by screen readers. Provide descriptive tooltip content that conveys the purpose or function of the element without relying solely on visual cues.
  5. Overlapping or Obscuring Content
    • Concern: Tooltips that overlap with or obscure other content on the page can create barriers for users with cognitive or motor disabilities.
    • Mitigation:ย Ensure tooltips are positioned dynamically to avoid overlapping with nearby content, especially when triggered near the edges of the viewport. Implement proper spacing and layout adjustments to prevent tooltips from obscuring important page elements, especially on smaller screens or in responsive designs.
Tested using
Chrome
with NVDA
Firefox
with NVDA
  • W3C SVG
    1.3.1
    Info and Relationships ( level A ) :
    Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
  • W3C SVG
    2.1.1
    Keyboard ( level A ) :
    All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints.
  • W3C SVG
    2.4.3
    Focus Order ( level A ) :
    If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability.
  • W3C SVG
    4.1.2
    Name, Role, Value ( level A ) :
    For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.