Dropdowns & Tooltips
Dropdown menus and tooltips both float above the page next to the thing that opened them. They look simple, but they are easy to get wrong: menus that only open for a mouse, tooltips a keyboard user can never see, panels hidden behind other content because of z-index.
In this lesson you will build a dropdown menu with the popover attribute, a CSS-only dropdown for simple navigation, and a tooltip that works for mouse, keyboard and screen reader users alike. You should know the Popover API and Positioning before you start.
A dropdown menu with popover
A dropdown is a button that opens a short list of choices. The popover attribute is the best starting point, because the browser does the hard parts for you:
- The
popovertargetbutton opens and closes the menu, with the mouse, Enter or Space. - Esc or a click outside closes it (light dismiss), and opening one popover closes any other.
- An open popover is drawn in the top layer, above everything else. No
z-index, and a parent withoverflow: hiddencannot clip it.
The one thing a popover cannot do on its own is sit next to its button. By default it appears in the middle of the window. In a header, where the button's position is known, you can pin the menu to a fixed spot with inset.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Popover dropdown</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.site-header {
position: sticky;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
height: 3.5rem;
padding-inline: 1rem;
background: #1e3a8a;
color: white;
}
.browse-btn {
padding: 0.4rem 0.9rem;
border: 2px solid white;
border-radius: 6px;
background: transparent;
color: white;
font: inherit;
cursor: pointer;
}
.browse-btn:focus-visible { outline: 3px solid #facc15; outline-offset: 2px; }
.browse-menu {
inset: 3.5rem 1rem auto auto; /* top right bottom left: under the right end of the header */
margin: 0;
min-width: 13rem;
padding: 0.4rem;
border: 1px solid #cbd5e1;
border-radius: 8px;
box-shadow: 0 10px 24px rgb(0 0 0 / 0.18);
}
.browse-menu ul { margin: 0; padding: 0; list-style: none; }
.browse-menu a {
display: block;
padding: 0.55rem 0.8rem;
border-radius: 6px;
color: #0f172a;
text-decoration: none;
}
.browse-menu a:hover { background: #e0e7ff; }
.browse-menu a:focus-visible { outline: 2px solid #1d4ed8; outline-offset: -2px; }
main { padding: 1rem; }
</style>
</head>
<body>
<header class="site-header">
<strong>Rohtak Book Depot</strong>
<button class="browse-btn" type="button" popovertarget="browse">
Browse books <span aria-hidden="true">▾</span>
</button>
<nav class="browse-menu" id="browse" popover aria-label="Book categories">
<ul>
<li><a href="/books/class-6-8">Class 6 to 8</a></li>
<li><a href="/books/class-9-10">Class 9 and 10</a></li>
<li><a href="/books/class-11-12">Class 11 and 12</a></li>
<li><a href="/books/competitive">Competitive exams</a></li>
</ul>
</nav>
</header>
<main>
<h1>New arrivals</h1>
<p>NCERT Maths for Class 10, ₹265. Previous year papers for SSC, ₹199.</p>
</main>
</body>
</html>Open the menu with the button, then press Esc or click on the page to close it. Try the keyboard too: Tab to the button, press Enter, then Tab into the links. The popover comes straight after its button in the HTML, so the Tab order runs from the button into the menu.
The small arrow is wrapped in <span aria-hidden="true">. Without that, some screen readers read the character's long Unicode name aloud.
A CSS-only dropdown with :hover and :focus-within
Some navigation bars show a sub-menu when you point at an item, with no button at all. You can build that with CSS alone. The sub-menu is a nested list, positioned under its parent item and hidden until the item is hovered or contains focus:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CSS-only dropdown</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.main-nav { background: #14532d; }
.main-nav ul { margin: 0; padding: 0; list-style: none; }
.main-nav > ul { display: flex; flex-wrap: wrap; padding-inline: 0.5rem; }
.main-nav a { display: block; padding: 0.8rem 1rem; color: white; text-decoration: none; }
.main-nav a:hover { background: #166534; }
.main-nav a:focus-visible { outline: 3px solid #facc15; outline-offset: -3px; }
.has-sub { position: relative; }
.sub {
display: none;
position: absolute;
top: 100%; /* touching the item: no gap for the pointer to fall into */
left: 0;
z-index: 10;
min-width: 11rem;
background: #166534;
border-radius: 0 0 8px 8px;
}
.has-sub:hover .sub,
.has-sub:focus-within .sub {
display: block;
}
main { padding: 1rem; }
</style>
</head>
<body>
<nav class="main-nav" aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li class="has-sub">
<a href="/branches">Branches</a>
<ul class="sub">
<li><a href="/branches/rohtak">Rohtak</a></li>
<li><a href="/branches/jaipur">Jaipur</a></li>
<li><a href="/branches/pune">Pune</a></li>
</ul>
</li>
<li><a href="/fees">Fees</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<main>
<h1>Pragati Tuition Centre</h1>
<p>Evening batches for Class 9 to 12 in three cities.</p>
</main>
</body>
</html>Point at "Branches" and the sub-menu appears. Now use only the keyboard: Tab to "Branches" and the sub-menu opens too, because :focus-within matches the <li> when anything inside it has focus. Keep tabbing and you move through the city links. Without :focus-within, keyboard users would never see those links.
This pattern works in every browser, which makes it a good fallback, but it has real limits. Touch screens have no real hover, so on many phones a tap on "Branches" just opens the Branches page and the sub-menu is never seen. The menu pops open whenever the pointer passes over it, Esc does nothing, and because it is not in the top layer it needs a z-index. For menus that people open on purpose, prefer the popover version.
Tooltips that everyone can use
A tooltip is a short piece of text that appears when a control is hovered or focused, such as "Drafts are kept for 30 days" next to a "Save draft" button. To work for everyone, a tooltip needs four things:
- It appears on keyboard focus, not only on hover. Use
:focus-withinor:focus-visiblealongside:hover. - Screen readers can hear it. Give the tooltip an
idandrole="tooltip", and point to it from the control witharia-describedby. The screen reader then reads the button's name followed by the tooltip text. - The pointer can move onto it without it vanishing, so people who zoom the page can read it.
- Esc hides it without the user having to move the mouse or the focus.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Accessible tooltip</title>
<style>
body { margin: 0; padding: 4.5rem 1.5rem 2rem; font-family: system-ui, sans-serif; }
.actions { display: flex; flex-wrap: wrap; gap: 1rem; }
.tip-wrap { position: relative; }
.tip-wrap button {
padding: 0.6rem 1.1rem;
border: 2px solid #1e3a8a;
border-radius: 6px;
background: white;
color: #1e3a8a;
font: inherit;
cursor: pointer;
}
.tip-wrap button:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
.tip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
translate: -50% 0;
width: max-content;
max-width: 14rem;
padding: 0.4rem 0.6rem;
border-radius: 6px;
background: #0f172a;
color: white;
font-size: 0.875rem;
opacity: 0;
visibility: hidden;
transition: opacity 150ms ease-out, visibility 150ms;
}
.tip::after { /* invisible bridge over the gap, so the pointer can reach the tip */
content: "";
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 8px;
}
.tip-wrap:hover .tip,
.tip-wrap:focus-within .tip {
opacity: 1;
visibility: visible;
}
.tip-wrap.tip-closed .tip { opacity: 0; visibility: hidden; }
</style>
</head>
<body>
<div class="actions">
<span class="tip-wrap">
<button type="button" aria-describedby="tip-draft">Save draft</button>
<span class="tip" role="tooltip" id="tip-draft">Drafts are kept for 30 days</span>
</span>
<span class="tip-wrap">
<button type="button" aria-describedby="tip-submit">Submit application</button>
<span class="tip" role="tooltip" id="tip-submit">You cannot change the form after submitting</span>
</span>
</div>
<script>
// Esc hides any visible tooltip; it shows again on the next hover or focus
const wraps = document.querySelectorAll(".tip-wrap");
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") wraps.forEach((wrap) => wrap.classList.add("tip-closed"));
});
wraps.forEach((wrap) => {
wrap.addEventListener("mouseenter", () => wrap.classList.remove("tip-closed"));
wrap.addEventListener("focusin", () => wrap.classList.remove("tip-closed"));
});
</script>
</body>
</html>Hover over a button, then move the pointer up onto the tooltip: it stays. Tab to a button and the tooltip appears; press Esc and it hides. The tooltip is hidden with opacity and visibility rather than display: none, so it can fade in with a transition. visibility: hidden also stops it from catching the mouse while it is invisible.
Keep tooltips short and never put anything essential or clickable inside them, because touch screen users may never see them. When help text matters on phones too, use a small button that opens a popover instead, like the PNR example in Popover API.
Common mistakes
- Setting
displayon a popover's class. That rule beats the browser's hidden style, so the menu shows all the time. Style the open state with:popover-openif you need a differentdisplay. - Hover-only menus and tooltips. Always pair
:hoverwith:focus-withinor:focus-visible. - Using the
titleattribute as a tooltip. It does not appear on keyboard focus or on touch screens, and you cannot style it. - A gap between a hover menu and its item. The pointer falls into the gap and the menu closes. Use
top: 100%and padding, not margin. - Adding
role="menu"to site navigation. That role is meant for application menus and promises arrow-key behaviour that you would have to build with JavaScript. A plain list of links is right for navigation.
Key points
- A
popoverdropdown opens from apopovertargetbutton, closes with Esc or an outside click, and sits in the top layer. - Without anchor positioning a popover is centred, so pin it with
insetor enhance it withposition-areainside@supports. - CSS-only dropdowns need
:focus-withinas well as:hover, and still fail on touch screens. - Link a tooltip to its control with
aria-describedbyandrole="tooltip", show it on focus as well as hover, and let Esc hide it. - Keep tooltips short and never put essential information or links inside them.
Exercise
A food delivery site has a header with a "Lucknow" city button on the right. Make the button open a popover listing four cities, pinned under the right end of the header with inset. Below the header, add a "Track order" button with a tooltip that says "Shows your rider on a map". The tooltip must appear on hover and keyboard focus, and be linked to the button with aria-describedby.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Delivery header</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
header {
display: flex;
align-items: center;
justify-content: space-between;
height: 3.5rem;
padding-inline: 1rem;
background: #9a3412;
color: white;
}
.city-btn { padding: 0.4rem 0.8rem; font: inherit; }
.city-menu { inset: 3.5rem 1rem auto auto; margin: 0; padding: 0.4rem; border: 1px solid #d1d5db; border-radius: 8px; }
.city-menu a { display: block; padding: 0.5rem 0.8rem; }
main { padding: 4rem 1rem 1rem; }
.tip-wrap { position: relative; }
.tip {
position: absolute;
bottom: calc(100% + 6px);
left: 0;
width: max-content;
padding: 0.3rem 0.6rem;
border-radius: 6px;
background: #111827;
color: white;
visibility: hidden;
}
.tip-wrap:hover .tip,
.tip-wrap:focus-within .tip { visibility: visible; }
</style>
</head>
<body>
<header>
<strong>Tiffin Express</strong>
<button class="city-btn" type="button" popovertarget="cities">Lucknow</button>
<div class="city-menu" id="cities" popover>
<a href="/city/lucknow">Lucknow</a>
<a href="/city/kanpur">Kanpur</a>
<a href="/city/jaipur">Jaipur</a>
<a href="/city/pune">Pune</a>
</div>
</header>
<main>
<span class="tip-wrap">
<button type="button" aria-describedby="track-tip">Track order</button>
<span class="tip" role="tooltip" id="track-tip">Shows your rider on a map</span>
</span>
</main>
</body>
</html>The city menu needs no script: popovertarget opens it and light dismiss closes it. The tooltip appears for both the mouse and the keyboard, and aria-describedby makes screen readers read it after the button's name. For the full version, add the Esc script from the lesson.
Quick Quiz
1. Which of these does a popover dropdown give you without any JavaScript?
Popovers handle light dismiss and the top layer; placing them next to the button needs inset or anchor positioning.
2. Why does a CSS-only hover dropdown also need :focus-within?
:focus-within matches the parent item when anything inside it has focus, so the sub-menu opens for keyboard users.
3. How should a tooltip be connected to the button it describes?
aria-describedby makes screen readers read the tooltip text after the button's name.
4. A popover menu is visible all the time, even before the button is clicked. What is the most likely cause?
Your display rule beats the browser's display: none for closed popovers. Use :popover-open to style the open state instead.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
