- Published on
The Ultimate Guide to Building a Simple Tooltip on Hover with HTML, CSS, and JavaScript
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
'The Ultimate Guide to Building a Simple Tooltip on Hover with HTML, CSS, and JavaScript'
Learn how to create beautiful, responsive, and accessible tooltips from scratch. This comprehensive guide covers everything from a CSS-only approach to advanced JavaScript positioning and accessibility best practices.
Table of Contents
- 'The Ultimate Guide to Building a Simple Tooltip on Hover with HTML, CSS, and JavaScript'
- The Ultimate Guide to Building a Simple Tooltip on Hover with HTML, CSS, and JavaScript
- What We'll Cover:
- Section 1: The Foundation - A Solid HTML Structure
- Section 2: The Visuals - The CSS-Only Approach
- Step 1: Positioning the Container
- Step 2: Creating the Tooltip with a Pseudo-Element
- Step 3: Creating the Triangle Arrow
- Step 4: Showing the Tooltip on Hover
- Section 3: Handling Different Positions
- Section 4: Leveling Up with JavaScript for Dynamic Positioning
- Step 1: Modifying the HTML and CSS
- Step 2: The JavaScript Logic
- Section 5: Don't Forget Accessibility (A11y)
- WAI-ARIA to the Rescue
- Keyboard Support
- Updated JavaScript with A11y
- Conclusion: Putting It All Together
The Ultimate Guide to Building a Simple Tooltip on Hover with HTML, CSS, and JavaScript
Tooltips are unsung heroes of user interface design. They're the quiet assistants that provide context, clarify icons, and offer extra information without cluttering the screen. When done right, they feel like a natural, intuitive part of the user experience. But when done wrong, they can be annoying, obstructive, and downright inaccessible.
In this comprehensive guide, we'll embark on a journey to build the perfect tooltip. We'll start with a simple, elegant solution using only HTML and CSS. Then, we'll level up with JavaScript to make our tooltips smarter, more responsive, and accessible to all users.
Whether you're a beginner looking to sharpen your front-end skills or a seasoned developer seeking a refresher on best practices, you're in the right place. Let's get building!
What We'll Cover:
- The HTML Foundation: Structuring our tooltip for success.
- The CSS-Only Approach: Styling a beautiful tooltip with a classic triangle pointer.
- Handling Multiple Positions: Creating modifier classes for top, right, bottom, and left tooltips.
- The JavaScript Enhancement: Dynamically positioning tooltips to avoid viewport collisions.
- Accessibility (A11y) is Key: Making our tooltips usable for everyone.
- The Final Code: A complete, production-ready solution.
Section 1: The Foundation - A Solid HTML Structure
Before we can style anything, we need a clean, semantic HTML structure. The core idea is to have a trigger element (the thing you hover over) and the tooltip content itself. A common and effective pattern is to use data-*
attributes to hold the tooltip text. This keeps our content and structure neatly separated.
Here’s a simple and robust structure:
<p>
Hover over this
<span class="tooltip-container" data-tooltip="I am a tooltip!">
magic word
</span>
to see the tooltip.
</p>
<p>
You can also add tooltips to buttons:
<button class="tooltip-container" data-tooltip="Click me to do something amazing.">
Hover Over Me
</button>
</p>
Why this structure?
tooltip-container
: This class identifies the elements that will trigger a tooltip. We'll use this as our hook for both CSS and JavaScript.data-tooltip
: This custom data attribute is the perfect place to store our tooltip's text. It's semantic, easy to access with CSS (attr()
) and JavaScript (dataset
), and doesn't interfere with styling or behavior.- Flexibility: This works on inline elements like
<span>
and block-level elements like<button>
or<div>
.
At this stage, nothing will happen visually. We've just laid the groundwork. Now, let's bring it to life with CSS.
Section 2: The Visuals - The CSS-Only Approach
For many simple use cases, a CSS-only tooltip is all you need. It's lightweight, fast, and doesn't require any JavaScript. The magic lies in using pseudo-elements (::before
and ::after
) and the :hover
pseudo-class.
Let's break down the CSS. First, we'll set up the container and the basic tooltip styles.
Step 1: Positioning the Container
We need to establish a positioning context for the tooltip. By setting the container to position: relative
, we can position the tooltip absolutely relative to it.
.tooltip-container {
position: relative;
cursor: pointer; /* Indicates to the user that it's interactive */
}
Step 2: Creating the Tooltip with a Pseudo-Element
We'll use the ::before
pseudo-element for the tooltip's body and ::after
for its triangular arrow. We'll grab the text directly from our data-tooltip
attribute using the attr()
CSS function.
/* Tooltip Body */
.tooltip-container::before {
content: attr(data-tooltip); /* Get the text from the data-attribute */
position: absolute;
bottom: 100%; /* Position it above the container */
left: 50%;
transform: translateX(-50%); /* Center the tooltip horizontally */
margin-bottom: 10px; /* Space between the element and the tooltip */
/* Styling */
background-color: #333;
color: #fff;
padding: 8px 12px;
border-radius: 5px;
font-size: 14px;
white-space: nowrap; /* Prevent the tooltip from breaking into multiple lines */
/* Hide it by default */
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 10;
}
Key Concepts Here:
content: attr(data-tooltip)
: This is the magic trick that pulls the text from our HTML into the CSS.position: absolute
: This takes the tooltip out of the normal document flow and allows us to position it precisely relative to thetooltip-container
.bottom: 100%
: This places the bottom edge of the tooltip at the top edge of the container.left: 50%; transform: translateX(-50%);
: This is a classic CSS technique for perfect horizontal centering of an absolutely positioned element.opacity: 0; visibility: hidden;
: This is the standard way to hide an element accessibly.opacity: 0
makes it invisible, whilevisibility: hidden
removes it from screen readers and prevents interaction. We'll toggle these on hover.transition
: This ensures a smooth fade-in/fade-out effect instead of an abrupt appearance.
Step 3: Creating the Triangle Arrow
No tooltip is complete without that little pointer arrow. We'll create this using the ::after
pseudo-element and a clever border trick.
/* Tooltip Arrow */
.tooltip-container::after {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 4px; /* Position it just below the tooltip body */
/* The triangle trick */
border-width: 6px;
border-style: solid;
border-color: #333 transparent transparent transparent;
/* Hide it by default */
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 10;
}
The triangle is formed by giving an empty element a thick border and then making three of the four border colors transparent. The remaining visible border forms the triangle.
Step 4: Showing the Tooltip on Hover
Finally, we use the :hover
pseudo-class on our container to reveal the tooltip and its arrow.
.tooltip-container:hover::before,
.tooltip-container:hover::after {
opacity: 1;
visibility: visible;
}
And just like that, you have a fully functional, stylish, CSS-only tooltip!
Section 3: Handling Different Positions
A tooltip that only appears on top isn't very flexible. What if our element is at the top of the page? The tooltip would be cut off. Let's create modifier classes to control the position.
We'll define classes like tooltip-top
, tooltip-right
, tooltip-bottom
, and tooltip-left
. Here's the HTML:
<span
class="tooltip-container tooltip-right"
data-tooltip="I'm on the right!"
>
Right Tooltip
</span>
Now for the CSS. We'll set up the default to be tooltip-top
and then override the properties for the other positions.
/* Default is Top */
.tooltip-container::before { ... } /* from before */
.tooltip-container::after { ... } /* from before */
/* -- Position Right -- */
.tooltip-container.tooltip-right::before {
bottom: auto;
top: 50%;
left: 100%;
transform: translateY(-50%);
margin-bottom: 0;
margin-left: 10px;
}
.tooltip-container.tooltip-right::after {
bottom: auto;
top: 50%;
left: 100%;
transform: translateY(-50%);
margin-bottom: 0;
margin-left: 4px;
border-color: transparent #333 transparent transparent;
}
/* -- Position Bottom -- */
.tooltip-container.tooltip-bottom::before {
top: 100%;
bottom: auto;
margin-bottom: 0;
margin-top: 10px;
}
.tooltip-container.tooltip-bottom::after {
top: 100%;
bottom: auto;
margin-bottom: 0;
margin-top: 4px;
border-color: transparent transparent #333 transparent;
}
/* -- Position Left -- */
.tooltip-container.tooltip-left::before {
bottom: auto;
top: 50%;
right: 100%;
left: auto;
transform: translateY(-50%);
margin-bottom: 0;
margin-right: 10px;
}
.tooltip-container.tooltip-left::after {
bottom: auto;
top: 50%;
right: 100%;
left: auto;
transform: translateY(-50%);
margin-bottom: 0;
margin-right: 4px;
border-color: transparent transparent transparent #333;
}
This is powerful, but it has a major weakness. We, the developers, have to manually decide the best position for the tooltip. If the user resizes their browser or uses a mobile device, our tooltip-right
might suddenly be pushed off the screen. This is where JavaScript comes to the rescue.
Section 4: Leveling Up with JavaScript for Dynamic Positioning
The biggest limitation of the CSS-only approach is its lack of awareness of the viewport. A truly robust tooltip should be smart enough to reposition itself to stay visible.
Our goal with JavaScript is to:
- Dynamically create the tooltip element instead of relying on pseudo-elements.
- Listen for mouse events.
- On hover, calculate the tooltip's position.
- Check if it will overflow the viewport.
- If it overflows, reposition it automatically.
Step 1: Modifying the HTML and CSS
Since JavaScript will now handle the creation and positioning, we can simplify our CSS. We no longer need the pseudo-elements. Instead, we'll create a real <span>
element for the tooltip.
Our HTML remains the same:
<span class="tooltip-container" data-tooltip="I am a smart tooltip!">Hover me</span>
Our new, simpler CSS:
.tooltip-container {
position: relative;
cursor: pointer;
}
.tooltip {
position: absolute;
background-color: #333;
color: #fff;
padding: 8px 12px;
border-radius: 5px;
font-size: 14px;
white-space: nowrap;
z-index: 10;
/* Hidden by default */
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
.tooltip.active {
opacity: 1;
visibility: visible;
}
/* We can still use a pseudo-element for the arrow! */
.tooltip::after {
content: '';
position: absolute;
border-width: 6px;
border-style: solid;
}
/* Arrow positions based on data-attribute set by JS */
.tooltip[data-position='top']::after {
top: 100%;
left: 50%;
transform: translateX(-50%);
border-color: #333 transparent transparent transparent;
}
.tooltip[data-position='bottom']::after {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-color: transparent transparent #333 transparent;
}
/* ... add right and left arrow styles as well */
Notice we're now using a .tooltip
class and an .active
class to control visibility. This gives JavaScript full control.
Step 2: The JavaScript Logic
Now for the main event. We'll write a script that initializes all tooltips on the page.
document.addEventListener('DOMContentLoaded', () => {
const tooltipTriggers = document.querySelectorAll('.tooltip-container');
tooltipTriggers.forEach(trigger => {
let tooltip = null; // To hold the tooltip element
trigger.addEventListener('mouseenter', () => {
const tooltipText = trigger.dataset.tooltip;
if (!tooltipText) return; // Don't do anything if there's no tooltip text
// 1. Create the tooltip element
tooltip = document.createElement('span');
tooltip.className = 'tooltip';
tooltip.textContent = tooltipText;
document.body.appendChild(tooltip);
// 2. Position the tooltip
positionTooltip(trigger, tooltip);
// 3. Show the tooltip
// We use a tiny timeout to allow the browser to paint the tooltip
// before we apply the transition class.
setTimeout(() => {
tooltip.classList.add('active');
}, 10);
});
trigger.addEventListener('mouseleave', () => {
if (tooltip) {
tooltip.classList.remove('active');
// Remove the tooltip from the DOM after the transition ends
tooltip.addEventListener('transitionend', () => {
if (tooltip) {
tooltip.remove();
tooltip = null;
}
});
}
});
});
});
function positionTooltip(trigger, tooltip) {
const triggerRect = trigger.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const gap = 10; // The space between the trigger and the tooltip
let top, left;
let position = 'top'; // Default position
// Try positioning on top
top = triggerRect.top - tooltipRect.height - gap;
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
// Check if it overflows the top
if (top < 0) {
position = 'bottom';
top = triggerRect.bottom + gap;
}
// Check for left/right overflow
if (left < 0) {
left = gap;
}
if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth - tooltipRect.width - gap;
}
tooltip.style.top = `${top + window.scrollY}px`;
tooltip.style.left = `${left + window.scrollX}px`;
tooltip.dataset.position = position;
}
What's happening in this script?
- We find all
.tooltip-container
elements. - On
mouseenter
, we create a<span>
with the tooltip text and append it to thedocument.body
. Appending to the body prevents positioning issues with parent elements that haveoverflow: hidden
. - The
positionTooltip
function is the core of our logic. It usesgetBoundingClientRect()
to get the dimensions and location of the trigger and the newly created tooltip. - It calculates the ideal 'top' position and then checks if that position is outside the viewport (
top < 0
). - If it overflows, it flips the position to 'bottom'.
- It also performs checks for horizontal overflow, ensuring the tooltip never goes off the left or right side of the screen.
- Finally, it applies the calculated
top
andleft
styles. We addwindow.scrollY
andwindow.scrollX
to account for page scrolling. - On
mouseleave
, we remove the.active
class to fade it out and then remove the element from the DOM entirely once the transition finishes.
This is a huge improvement! Our tooltips are now context-aware and much more user-friendly.
Section 5: Don't Forget Accessibility (A11y)
A tooltip that only works on hover is inaccessible to keyboard-only users and can be confusing for screen reader users. This is a critical step that turns a good tooltip into a great one.
WAI-ARIA to the Rescue
The Web Accessibility Initiative’s Accessible Rich Internet Applications (WAI-ARIA) spec gives us the tools we need.
role="tooltip"
: We'll add this to our tooltip element to tell screen readers what it is.aria-describedby
: We'll add this to the trigger element. It points to the ID of the tooltip, creating a programmatic link between them. This tells screen readers, "The description for this element can be found in that other element."
Keyboard Support
We need to show the tooltip when the trigger element receives focus (e.g., via the Tab
key) and hide it when it loses focus.
We'll use the focusin
and focusout
events. These are better than focus
and blur
because they bubble, which makes event delegation simpler.
Updated JavaScript with A11y
Let's integrate these improvements into our script.
// ... (keep the positionTooltip function as is) ...
document.addEventListener('DOMContentLoaded', () => {
const tooltipTriggers = document.querySelectorAll('.tooltip-container');
tooltipTriggers.forEach((trigger, index) => {
const tooltipId = `tooltip-${index}`;
trigger.setAttribute('aria-describedby', tooltipId);
let tooltip = null;
const showTooltip = () => {
const tooltipText = trigger.dataset.tooltip;
if (!tooltipText) return;
tooltip = document.createElement('span');
tooltip.id = tooltipId;
tooltip.className = 'tooltip';
tooltip.setAttribute('role', 'tooltip');
tooltip.textContent = tooltipText;
document.body.appendChild(tooltip);
positionTooltip(trigger, tooltip);
setTimeout(() => {
tooltip.classList.add('active');
}, 10);
};
const hideTooltip = () => {
if (tooltip) {
tooltip.classList.remove('active');
tooltip.addEventListener('transitionend', () => {
tooltip.remove();
tooltip = null;
}, { once: true }); // Ensure the event listener is removed after firing
}
};
// Use focusin/focusout for keyboard and mouseenter/mouseleave for mouse
trigger.addEventListener('mouseenter', showTooltip);
trigger.addEventListener('focusin', showTooltip);
trigger.addEventListener('mouseleave', hideTooltip);
trigger.addEventListener('focusout', hideTooltip);
// Bonus: Hide tooltip on 'Escape' key press
trigger.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
hideTooltip();
}
});
});
});
A11y Enhancements:
- We now dynamically generate a unique
id
for each tooltip and setaria-describedby
on the trigger to point to it. - The tooltip element gets
role="tooltip"
. - We've abstracted the
showTooltip
andhideTooltip
logic into their own functions. - We now listen for
focusin
andfocusout
in addition tomouseenter
andmouseleave
, providing full support for both mouse and keyboard users. - As a nice UX touch, pressing the
Escape
key while focused on the trigger will now also dismiss the tooltip.
Conclusion: Putting It All Together
We've come a long way! We started with a simple HTML structure, built a stylish CSS-only tooltip, and then supercharged it with JavaScript for dynamic positioning and robust accessibility.
You now have a powerful, reusable, and professional-grade tooltip solution that you can drop into any project. The final result is a component that not only looks good but also provides a seamless experience for all users, regardless of how they browse the web.
Remember, the best UI components are the ones that feel helpful but never get in the way. By combining thoughtful CSS, smart JavaScript, and a commitment to accessibility, you can create tooltips that truly enhance your user's journey.
Happy coding!