Tooltips
Demo
<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?
- 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.
- 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.
- 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.
- 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.
- 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.
- Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
- 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.
- 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.
- 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.