Infotip
An infotip consists of two parts:
- Trigger element.
- Floating text bubble.
The floating text bubble can display when the trigger element is:
- Hovered over with the mouse.
- Focused via keyboard.
- Activated (by mouse click, by tap, or via the Enter or Spacebar key when the trigger has keyboard focus).
Here is an example infotip: An infotip consists of a trigger and a floating text bubble, this is the bubble.
For compliance to WCAG 2.2, the following four success criteria are especially relevant:
- 2.1.1 Keyboard : the keyboard focus can be moved to the trigger and the infotip displayed via keyboard. The infotip can display directly on keyboard focus or when the Enter or Spacebar keys are pressed.
- 4.1.2 Name, Role, Value : the infotip is accessible via screen reader and it is conveyed via text or markup that the content is an infotip.
- 1.4.13 Content on Hover or Focus : for infotips that expand on mouse hover or keyboard focus: the mouse cursor can be moved across the infotip without the infotip closing. In addition, pressing the Escape key closes the infotip.
- 2.4.11 Focus Not Obscured (Minimum) : for infotips that expand on mouse click or keyboard press (Enter or Spacebar keys), pressing the Escape key closes the infotip.
The implementations below are three examples of WCAG compliant infotips.
Button trigger infotip
This infotip is implemented as typical expanding content: the trigger is a button with an 'aria-expanded' attribute and the expanding content (displayed as a floating bubble) is placed in the DOM sequence immediately after the trigger. When the infotip is displayed, the value of 'aria-expanded' is "true" and when closed the value is "false". The trigger buttons label is "infotip".
Note that the functionality of the trigger button is solely to display/close the infotip.
Example: An infotip consists of a trigger and a floating text bubble, this is the bubble.
Markup and code example
HTML
Example:
<span class="
<!-- SVG expands when bubble is displayed, so mouseleave is not triggered when moving cursor from trigger to bubble -->
<svg class="
<path class="
<animate class="
<animate class="
</path>
</svg>
<button aria-label="
<svg>
<circle class="
<path class="
<circle class="
</svg>
</button>
<span class="
An
<a href="
infotip
</a>
consists of a trigger and a floating text bubble, this is the bubble.
</span>
</span>
</p>
CSS
position:relative;
top:
display:inline-flex;
padding:
margin:
width:
height:
> button{
position:absolute;
padding:
margin:
width:
height:
border:
border-radius:
outline-offset:
svg{
width:
height:
path.bubble {
fill: #292929;
}
circle.circle {
fill: #292929;
cursor:pointer;
}
path.mark {
stroke:#FFFFFF;
stroke-width:
fill:none;
cursor:pointer;
}
circle.dot{
fill:#FFFFFF;
cursor:pointer;
}
}
}
svg.back{
position:absolute;
left:
bottom:
cursor:auto;
}
span.con{
z-index:
position:absolute;
display:block;
visibility:hidden;
width:fit-content;
height:fit-content;
max-height:fit-content;
padding:
font-size:
font-family:Verdana;
color:#FFF;
a {
color:white;
}
}
}
JavaScript
const It
const Btt
const Con
const SVG
const Show
const Hide
const Bub
//Content <span>. The width is fixed
let ConX;
let ConY;
const ConW
let ConH;
Con.style.width
//SVG
let SvgX;
let SvgY;
let SvgW;
let SvgH;
//Space between button and content/bubble
const Space
//Button width & height
const Bwh
//Prevent trigger of expand/collapse mid-animation
let Moving
//Prevent mouse hover functionality when display of help tip is by clicking the button
let NoMouse
//Stats (show/hide) is changed by: button-click, mouseenter, mouseleave, Escape-key, and page-click
//Each event handler checks the state of aria-expanded so showTip() and hideTip() do not need to check this,
//showTip() and hideTip() just set the state of aria-expanded
Btt.addEventListener("click",()=>{
if(Btt.getAttribute("aria-expanded")
showTip(true);
}else{
hideTip();
}
});
It.addEventListener("mouseenter",()=>{
//Delay show on mousehover a little bit so it doesn't pop up when user is just moving cursor across screen
setTimeout(function(){
if(NoMouse){return}
//After delay, only show if cursor is still over trigger
if([
if(Btt.getAttribute("aria-expanded")
showTip();
}
}
},50);
});
It.addEventListener("mouseleave",()=>{
if(NoMouse){return}
if(Btt.getAttribute("aria-expanded")
hideTip();
}
});
//The following two functions are only active when the tip is open, so no need to check the value of 'aria-expanded'
const keyClose
if(ev.key
hideTip();
}
}
const mouseClose
//Ignore any click on the help tip
//except if the click is on the SVG between button and bubble.
if(It.contains(ev.target)
return;
}
hideTip();
}
//================================================
const showTip
if(Moving){return}
Moving
if(SetNoMouse){
NoMouse
}
Btt.setAttribute("aria-expanded","true");
ConH
const winW
const bcl
//Default values when content is to the right or left of button
SvgW
SvgH
//If enough space above button, con mid is level with button mid
if(bcl.top
ConY
SvgY
//else, con top is level with button top
}else{
ConY
SvgY
}
//If enough space to the right of the button
if(winW
ConX
SvgX
//else, if enough space to the left of the button
}else if(bcl.left
ConX
SvgX
//Else, place content below button, centred in screen
}else{
//Content is centred horizontally in screen
ConX
ConY
SvgY
SvgH
//=== Remember === conX is relative to button left edge.
//If button left edge is to the left of content left edge.
if(ConX
SvgX
SvgW
//If button right edge is to the right of content right edge.
}else if(
SvgX
SvgW
//If button is between left and right side of con
}else{
SvgX
SvgW
}
}
SVG.style.left
SVG.style.top
SVG.style.width
SVG.style.height
//d-attribute value for bubble path
let bubStr
Show.setAttributeNS(null,"to", bubStr);
Hide.setAttributeNS(null,"from", bubStr);
SVG.setAttribute("viewBox", SvgX
Show.beginElement();
}
Show.addEventListener("endEvent", () => {
Con.style.left
Con.style.top
Con.style.visibility
document.body.addEventListener("keydown",keyClose);
document.body.addEventListener("click",mouseClose);
Moving
});
//================================================
const hideTip
if(Moving){return}
Moving
Btt.setAttribute("aria-expanded","false");
//If the focus is on the bubble, place focus on the trigger button when bubble closes
if(Con.contains(document.activeElement)){
Btt.focus();
}
Con.style.visibility
Hide.beginElement();
}
Hide.addEventListener("endEvent", () => {
SVG.style.left
SVG.style.top
SVG.style.width
SVG.style.height
SVG.setAttribute("viewBox", "0 0 18 18");
document.body.removeEventListener("keydown",keyClose);
document.body.removeEventListener("click",mouseClose);
NoMouse
Moving
});
//==========================================================
function drawBub(x,y,w,h,r){
d
//8 points. Each triplet is: controlpoint, controlpoint, endpoint
var p
let ss
for(let i
ss
}
return ss;
}
const around
if(
const factor
return Math.round((num
}
}
Markup and code example, no animation
HTML
Example:
<span class="
<!-- backdrop expands when bubble is displayed, so mouseleave is not triggered when moving cursor from trigger to bubble -->
<span class="
<button aria-label="
<svg>
<circle class="
<path class="
<circle class="
</svg>
</button>
<span class="
An
<a href="
infotip
</a>
consists of a trigger and a floating text bubble, this is the bubble.
</span>
</span>
</p>
CSS
position:relative;
top:
display:inline-flex;
padding:
margin:
width:
height:
span.back{
position:absolute;
left:
bottom:
cursor:auto;
}
> button{
position:absolute;
padding:
margin:
width:
height:
border:
border-radius:
outline-offset:
svg{
width:
height:
circle.circle {
fill: #292929;
cursor:pointer;
}
path.mark {
stroke:#FFFFFF;
stroke-width:
fill:none;
cursor:pointer;
}
circle.dot{
fill:#FFFFFF;
cursor:pointer;
}
}
}
span.con{
z-index:
position:absolute;
display:block;
width:fit-content;
height:fit-content;
max-height:fit-content;
padding:
font-size:
font-family:Verdana;
color:#FFFFFF;
background:#292929;
border-radius:
a {
color:#FFFFFF;
}
}
}
JavaScript
const It
const Btt
const Con
const Back
const Bub
//Content <span>. The width is fixed
let ConX
let ConY
const ConW
let ConH
Con
//Backdrop
let BackX
let BackY
let BackW
let BackH
//Space between button and content/bubble
const Space
//Button width & height
const Bwh
//Prevent mouse hover functionality when display of help tip is by clicking the button
let NoMouse
//Stats (show/hide) is changed by: button-click, mouseenter, mouseleave, Escape-key, and page-click
//Each event handler checks the state of aria-expanded so showTip() and hideTip() do not need to check this,
//showTip() and hideTip() just set the state of aria-expanded
Btt
if
showTip
hideTip
It
//Delay show on mousehover a little bit so it doesn't pop up when user is just moving cursor across screen
setTimeout
if
//After delay, only show if cursor is still over trigger
if
if
showTip
It
if
if
hideTip
//The following two functions are only active when the tip is open, so no need to check the value of 'aria-expanded'
const keyClose
if
hideTip
const mouseClose
//Ignore any click on the help tip
//except if the click is on the SVG between button and bubble.
if
return
hideTip
//================================================
const showTip
if
NoMouse
Btt
ConH
const winW
const bcl
//Default values when content is to the right or left of button
BackW
BackH
//If enough space above button, con mid is level with button mid
if
ConY
BackY
//else, con top is level with button top
ConY
BackY
//If enough space to the right of the button
if
ConX
BackX
//else, if enough space to the left of the button
ConX
BackX
//Else, place content below button, centred in screen
//Content is centred horizontally in screen
ConX
ConY
BackY
BackH
//=== Remember === conX is relative to button left edge.
//If button left edge is to the left of content left edge.
if
BackX
BackW
//If button right edge is to the right of content right edge.
BackX
BackW
//If button is between left and right side of con
BackX
BackW
Back
Back
Back
Back
Con
Con
Con
document
document
//================================================
const hideTip
Btt
//If the focus is on the bubble, place focus on the trigger button when bubble closes
if
Btt
Con
Back
Back
Back
Back
document
document
NoMouse
Usability considerations
- On mouse hover, the infotip opens after a slight delay. This ensures the infotip does not open when users simply moves the cursor across the screen.
- When the infotip is opened by activating the trigger, it remains open even when the cursor is moved away from the infotip. This helps users who have difficulty controlling the mouse movement, as the bubble will remain open if the user accidently moves the cursor away.
- If the keyboard focus is on the bubble when it closes, the focus is placed back on the trigger. This ensures the focus does not move to a random place on the page when the infotip closes.
- Clicking on the page (except on the bubble itself) closes the infotip.
Button trigger infotip, with close button
This infotip is implemented as above, with the addition of a close button.
The button is placed at the end of the bubble content in the DOM sequence and is therefore read last by screen readers. This helps screen reader users identify where the infotip ends. The label for the button is "Close infotip".
Example:
Button trigger infotip, no mouse hover
This infotip is implemented as above, except it is not displayed on mouse hover.
Example: An infotip consists of a trigger and a floating text bubble, this is the bubble.
Image trigger infotip
This implementation can be useful when retrofitting accessibility to existing infotips; however, it dplays the infotip on keyboard focus and this can be distracting to keyboard-only users who simply navigate through the content by pressing the Tab key. The button trigger infotip above is a more user friendly option.
For this infotip, the trigger is implemented as an image that is focusable via the tabindex="0" attribute. The infotip is displayed on keyboard focus and on mouse hover.
The alt-text for the image contains the infotip text with the addition of the phrase "infotip:". This communicates to screen reader users that the content is an infotip. In addition, it explains to screen reader users why the image is keyboard focusable (images are not usually focusable).
In this implementation, two "versions" of the infotip content exists, the:
- Image alt-text, accessed by screen reader users.
- Visible infotip (floating bubble), accessed by other users.
As the infotip content is provided to screen readers via an attribute value (text-only content), the:
- Infotip cannot include interactive controls (e.g. links).
- Visible infotip should be inserted in the DOM sequence at the bottom of the page, rather than straight after the trigger. This avoids potential repetition of the infotip for screen reader users.
Example:
Markup and code example
HTML
Example:
<span class="
<svg role="
<circle class="
<path class="
<circle class="
</svg>
</span>
</p>
CSS
position:relative;
top:
display:inline-flex;
padding:
margin:
width:
height:
svg{
width:
height:
border-radius:
outline-offset:
path.bubble {
fill: #292929;
}
circle.circle {
fill: #292929;
cursor:pointer;
}
path.mark {
stroke:#FFFFFF;
stroke-width:
fill:none;
cursor:pointer;
}
circle.dot{
fill:#FFFFFF;
cursor:pointer;
}
}
}
body > span > svg.back{
z-index:
position:fixed;
left:
bottom:
cursor:auto;
}
body > span > svg.back > path.bubble {
fill: #292929;
}
body > span > span.con{
z-index:
position:fixed;
display:block;
height:fit-content;
padding:
font-size:
font-family:Verdana;
color:#FFF;
a {
color:white;
}
}
JavaScript
const It
const Qmark
let SVG;
let Show;
let Hide;
let Con;
//Content <span>. The width is fixed
let ConX;
let ConY;
const ConW
let ConH;
//SVG
let SvgX;
let SvgY;
let SvgW;
let SvgH;
//Space between button and content/bubble
const Space
//Button width & height
const Bwh
//Prevent trigger of expand/collapse mid-animation
let Moving
let Expanded
let ClosedByEscape
let ClientX;
let ClientY;
//===========================================================
It.addEventListener("mouseenter",()=>{
//Delay show on mousehover a little bit so it doesn't pop up when user is just moving cursor across screen
setTimeout(function(){
//After delay, only show if cursor is still over trigger
if([
if(ClosedByEscape){
ClosedByEscape
return;
}
showTip();
}
},50);
});
Qmark.addEventListener("focus",()=>{
showTip();
});
Qmark.addEventListener("blur",()=>{
hideTip();
});
//====
const keyClose
if(ev.key
//Set ClosecByEscape set to true if mouse cursor is above trigger. This means the mouseenter that
//is triggered when bubble closes is ignored and we avoid bubble opening up again.
let bcl
if(ClientX
ClosedByEscape
}
hideTip();
}
}
const mouseClose
//Ignore any click on the help tip,
//except if the click is on the SVG between button and bubble.
if(Span.contains(ev.target)
return;
}
//Ignore if the click is on the trigger. If clicking on trigger closes the infotip, it
//will immediately open again as mouseenter is triggered.
let bcl
if(ClientX
return;
}
hideTip();
}
//Called on mousemove
const mouseTrack
ClientX
ClientY
}
//Called on scroll
const adjustPosition
const bcl
SVG.style.left
SVG.style.top
Con.style.left
Con.style.top
}
//================================================
//================================================
const showTip
if(Moving){return}
if(Expanded){return}
Moving
const ns
Span
SVG
SVG.setAttribute("class","back");
SVG.setAttribute("viewBox","0 0 18 18");
let path
path.setAttribute("class","bubble");
SVG.appendChild(path);
Show
Show.setAttribute("class","show");
Show.setAttribute("attributeName","d");
Show.setAttribute("begin","indefinite");
Show.setAttribute("dur",".18");
Show.setAttribute("repeatCount","1");
Show.setAttribute("fill","freeze");
path.appendChild(Show);
Hide
Hide.setAttribute("class","hide");
Hide.setAttribute("attributeName","d");
Hide.setAttribute("begin","indefinite");
Hide.setAttribute("dur",".18");
Hide.setAttribute("repeatCount","1");
Hide.setAttribute("fill","freeze");
path.appendChild(Hide);
Span.appendChild(SVG);
Con
Con.setAttribute("class","con");
Con.style.visibility
Con.style.width
let s
if(s.indexOf("Infotip: ")
s
}
Con.textContent
Span.appendChild(Con);
Span.addEventListener("mouseleave",()=>{
hideTip();
});
document.body.appendChild(Span);
ConH
const winW
const bcl
//Default values when content is to the right or left of button
SvgW
SvgH
//If enough space above button, con mid is level with button mid
if(bcl.top
ConY
SvgY
//else, con top is level with button top
}else{
ConY
SvgY
}
//If enough space to the right of the button
if(winW
ConX
SvgX
//else, if enough space to the left of the button
}else if(bcl.left
ConX
SvgX
//Else, place content below button, centered in screen
}else{
//Content is centered horizontally in screen
ConX
ConY
SvgY
SvgH
//=== Remember === conX is relative to button left edge.
//If button left edge is to the left of content left edge.
if(ConX
SvgX
SvgW
//If button right edge is to the right of content right edge.
}else if(
SvgX
SvgW
//If button is between left and right side of con
}else{
SvgX
SvgW
}
}
SVG.style.left
SVG.style.top
SVG.style.width
SVG.style.height
SVG.setAttribute("viewBox", SvgX
//Start size is a little bubble in the center
let dStart
path.setAttribute("d", dStart);
Show.setAttribute("from", dStart);
Hide.setAttribute("to", dStart);
//End size is big bubble to contain the textt
let dEnd
Show.setAttribute("to", dEnd);
Hide.setAttribute("from", dEnd);
//===========================================
Show.addEventListener("endEvent", () => {
Con.style.left
Con.style.top
Con.style.visibility
document.body.addEventListener("keydown",keyClose);
document.body.addEventListener("click",mouseClose);
document.body.addEventListener("mousemove",mouseTrack);
window.addEventListener("scroll",adjustPosition);
Expanded
Moving
});
Hide.addEventListener("endEvent", () => {
Span.remove();
document.body.removeEventListener("keydown",keyClose);
document.body.removeEventListener("click",mouseClose);
document.body.addEventListener("mousemove",mouseTrack);
window.removeEventListener("scroll",adjustPosition);
//Get all, just in case..
document.querySelectorAll("body > svg.back").forEach(el => el.remove());
document.querySelectorAll("body > span.con").forEach(el => el.remove());
Expanded
Moving
});
Show.beginElement();
}
//================================================
//================================================
const hideTip
if(Moving){return}
if(
Moving
Con.style.visibility
Hide.beginElement();
}
//==========================================================
function drawBub(x,y,w,h,d){
if(
var h2
//8 points. Each triplet is: controlpoint, controlpoint, endpoint
var p
let ss
for(let i
ss
}
return ss;
}
const around
if(
const factor
return Math.round((num
}
}
Button trigger infotip via popover
The infotips above are implemented via JavaScript (and SVG animations); however, accessible infotips can also be implemented via the Popover API , with minimal coding.
On desktop, a popover infotip provides the same level of keyboard and screen reader access regardless of the bubble position in the DOM sequence (e.g. straight after the trigger button or at the bottom of the page); however, this is not supported on mobile devices, and therefore, the infotip must be inserted in the DOM sequence immediately after the trigger.
Here is an example using popover: An infotip consists of a trigger and a floating text bubble, this is the bubble.
Markup and code example
HTML
Here is an example using popover:
<button class="
<svg>
<circle class="
<path class="
<circle class="
</svg>
</button>
<span id="
An
<a href="
infotip
</a>
consists of a trigger and a floating text bubble, this is the bubble.
</span>
</p>
CSS
position:relative;
top:
padding:
margin:
width:
height:
border:
border-radius:
outline-offset:
svg{
width:
height:
circle.circle {
fill: #292929;
cursor:pointer;
}
path.mark {
stroke:#FFFFFF;
stroke-width:
fill:none;
cursor:pointer;
}
circle.dot{
fill:#FFFFFF;
cursor:pointer;
}
}
}
#bubble6 {
width:
height:fit-content;
padding:
font-size:
font-family:Verdana;
color:#FFF;
background:#292929;
border-radius:
a {
color:white;
}
}
#bubble6:popover-open {
/*Default is "auto" which positions it in the middle.*/
margin:
border:
}
JavaScript
bubble6.addEventListener("toggle", (ev) => {
if(ev.newState
positionPopover();
//Turning the visibility on and off is not strictlhy needed, it's only added to
//avoid flicker when the popover is displayed
bubble6.style.visibility
window.addEventListener("scroll",positionPopover);
}else{
window.removeEventListener("scroll",positionPopover);
bubble6.style.visibility
}
});
//Space between button and content/bubble
const Space
//Button width & height
const Bwh
const Btt
const positionPopover
const conW
const conH
const winW
const bcl
//Bubble coordinates
let x;
let y;
//If enough space above button, bubble middel is level with button mid
if(bcl.top
y
//else, bubble top is level with button top
}else{
y
}
//If enough space to the right of the button
if(winW
x
//else, if enough space to the left of the button
}else if(bcl.left
x
//Else, place content below button, centered in screen
}else{
x
y
}
bubble6.style.left
bubble6.style.top
}
}
By default, the bubble is positioned in the centre of the screen. The API includes a CSS anchor functionality that can position the bubble close to the trigger; however, this is not fully supported by browsers, so the positioning for this example is managed via scripting.
Terms of Use
This software is being provided "as is" - without any express or implied warranty. In particular, me2 Accessibility does not make any representation or warranty of any kind concerning the reliability, quality, or merchantability of this software or its fitness for any particular purpose. In addition, me2 Accessibility does not guarantee that use of this tool will ensure the accessibility of your web content or that your web content will comply with any specific accessibility standard.
This work is licensed under a
Creative Commons License