Multi-Level Dropdown Menu Without JavaScript
Three levels, reachable by mouse and keyboard, every link in the HTML for search engines — built with nested lists and about 110 lines of CSS. Live demo and copy-paste code below.
- JavaScript
- 0 KB
- Built with
:focus-within,:has(), container queries- Browsers
- All current browsers (Chrome, Edge, Firefox, Safari)
- Updated
No. A nested list of links plus two CSS selectors does it: :hover opens a submenu for mouse users and :focus-within opens it for keyboard users. The links are in the HTML from the start, so the menu works before any script loads — and search engines can follow every link.
Live demo
This is a real menu of this site: every link goes to a live page. Hover over Patterns or Free tools, or click into the demo and press Tab to move through it with the keyboard. You can switch off JavaScript in your browser — nothing changes.
On a narrow screen, every level is listed open.
How the CSS-only dropdown works
- Nested lists are the hierarchy
Each submenu is a
<ul>inside the<li>of its parent link. Browsers, screen readers and crawlers all understand this structure without any extra markup. - Submenus are hidden with
visibilityopacity: 0plusvisibility: hiddenhides a submenu, keeps it out of the keyboard tab order, and still allows a fade animation —display: nonecan't be animated as easily. :hoverand:focus-withinopen it:hovercovers the mouse.:focus-withinmatches a list item while any link inside it has keyboard focus, so tabbing onto “Patterns” opens its submenu before the next Tab moves into it.- A small delay forgives slips
The
visibilitytransition is delayed by 0.2 s when closing. If the mouse slips off a submenu for a moment, it doesn't snap shut. :has()adds the arrowsli:has(> ul)finds items that contain a submenu, so the ▾ and › arrows need no extra classes in the HTML.- A container query handles small screens
Under 40rem of available width, every level is shown open and indented. Touch users never depend on hover. It reacts to the menu's own width, so it also works in sidebars.
Copy the HTML and CSS
This is exactly the code running in the demo above. The links point to real pages on this site — replace them with your own. All class names start with ui-, so they won't clash with your existing styles.
<nav class="ui-dropdown" aria-label="CSS-first UI patterns">
<ul>
<li>
<a href="/lab/css-first-ui/">Patterns</a>
<ul>
<li>
<a href="/lab/css-first-ui/#navigation">Navigation</a>
<ul>
<li><a href="/lab/css-first-ui/dropdown-menu-without-javascript/" aria-current="page">Dropdown menu</a></li>
<li><a href="/lab/css-first-ui/css-mega-menu/">Mega menu</a></li>
<li><a href="/lab/css-first-ui/hamburger-menu-without-javascript/">Hamburger menu</a></li>
<li><a href="/lab/css-first-ui/breadcrumb-navigation-schema/">Breadcrumbs</a></li>
</ul>
</li>
<li>
<a href="/lab/css-first-ui/#structure">Page structure</a>
<ul>
<li><a href="/lab/css-first-ui/table-of-contents-anchor-links/">Table of contents</a></li>
<li><a href="/lab/css-first-ui/pagination-seo/">Pagination</a></li>
<li><a href="/lab/css-first-ui/language-switcher-hreflang/">Language switcher</a></li>
</ul>
</li>
<li>
<a href="/lab/css-first-ui/#content">Content</a>
<ul>
<li><a href="/lab/css-first-ui/accordion-without-javascript/">Accordion</a></li>
<li><a href="/lab/css-first-ui/tabs-without-javascript/">Tabs</a></li>
<li><a href="/lab/css-first-ui/read-more-without-javascript/">Read more</a></li>
<li><a href="/lab/css-first-ui/filter-without-javascript/">Filter</a></li>
</ul>
</li>
<li>
<a href="/lab/css-first-ui/#overlays">Overlays</a>
<ul>
<li><a href="/lab/css-first-ui/modal-without-javascript/">Modal</a></li>
<li><a href="/lab/css-first-ui/css-lightbox/">Lightbox</a></li>
<li><a href="/lab/css-first-ui/css-tooltip/">Tooltip</a></li>
</ul>
</li>
<li>
<a href="/lab/css-first-ui/#forms-media">Media & forms</a>
<ul>
<li><a href="/lab/css-first-ui/css-carousel-scroll-snap/">Carousel</a></li>
<li><a href="/lab/css-first-ui/lazy-loading-images-without-javascript/">Lazy loading</a></li>
<li><a href="/lab/css-first-ui/scroll-animations-without-javascript/">Scroll animations</a></li>
<li><a href="/lab/css-first-ui/form-validation-without-javascript/">Form validation</a></li>
</ul>
</li>
</ul>
</li>
<li>
<a href="/tools/">Free tools</a>
<ul>
<li>
<a href="/tools/#website-kit">Website Kit</a>
<ul>
<li><a href="/tools/chinese-font-stack-generator/">Chinese font stack</a></li>
<li><a href="/tools/chinese-font-subset/">Chinese font subset</a></li>
<li><a href="/tools/pinyin-slug-generator/">Pinyin URL slugs</a></li>
<li><a href="/tools/chinese-filler-text/">Chinese dummy text</a></li>
</ul>
</li>
<li>
<a href="/tools/#baidu-kit">Baidu Kit</a>
<ul>
<li><a href="/tools/baidu-serp-preview/">Baidu SERP preview</a></li>
<li><a href="/tools/chinese-seo-editor/">Chinese SEO editor</a></li>
</ul>
</li>
</ul>
</li>
<li>
<a href="/services/seo/china/">Services</a>
<ul>
<li><a href="/services/seo/china/">Baidu SEO</a></li>
<li><a href="/services/geo/china/">Chinese GEO</a></li>
<li><a href="/services/ppc/">Chinese PPC</a></li>
</ul>
</li>
</ul>
</nav>/* Multi-level dropdown menu — no JavaScript.
Level 2 opens below, level 3 opens to the side.
In narrow spaces (under 40rem) every level is listed open. */
.ui-dropdown {
--ui-bg: #ffffff;
--ui-text: #1b1f3b;
--ui-accent: #4f46e5;
--ui-hover: #eef0ff;
--ui-border: #dfe2f0;
container-type: inline-size;
font: 500 0.9375rem/1.4 system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ui-dropdown ul {
margin: 0;
padding: 0;
list-style: none;
}
.ui-dropdown li {
position: relative;
}
.ui-dropdown a {
display: block;
padding: 0.75rem 1rem;
color: var(--ui-text);
text-decoration: none;
white-space: nowrap;
}
.ui-dropdown a:hover,
.ui-dropdown a:focus-visible {
color: var(--ui-accent);
background: var(--ui-hover);
}
.ui-dropdown a:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: -2px;
}
.ui-dropdown a[aria-current="page"] {
color: var(--ui-accent);
font-weight: 700;
}
/* Level 1: horizontal bar */
.ui-dropdown > ul {
display: flex;
flex-wrap: wrap;
background: var(--ui-bg);
border-radius: 10px;
}
/* Arrows on items that have a submenu (:has needs no extra class) */
.ui-dropdown li:has(> ul) > a::after {
content: " ▾";
font-size: 0.8em;
}
.ui-dropdown li li:has(> ul) > a::after {
content: " ›";
}
/* Levels 2 and 3: hidden, but still in the HTML for crawlers */
.ui-dropdown li > ul {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
min-width: 13rem;
padding: 0.375rem 0;
background: var(--ui-bg);
border: 1px solid var(--ui-border);
border-radius: 10px;
box-shadow: 0 12px 32px rgb(15 18 40 / 0.18);
opacity: 0;
visibility: hidden;
transform: translateY(6px);
/* the visibility delay keeps the menu open for a moment
when the mouse slips off it */
transition: opacity 0.15s, transform 0.15s, visibility 0s 0.2s;
}
.ui-dropdown li li > ul {
top: -0.375rem;
left: 100%;
transform: translateX(6px);
}
/* Open on mouse hover AND on keyboard focus */
.ui-dropdown li:hover > ul,
.ui-dropdown li:focus-within > ul {
opacity: 1;
visibility: visible;
transform: none;
transition-delay: 0s;
}
/* Narrow space (phones, sidebars): show the whole tree, no hover needed */
@container (max-width: 40rem) {
.ui-dropdown > ul {
flex-direction: column;
}
.ui-dropdown li > ul,
.ui-dropdown li li > ul {
position: static;
min-width: 0;
padding: 0 0 0 1rem;
border: 0;
border-radius: 0;
box-shadow: none;
opacity: 1;
visibility: visible;
transform: none;
}
.ui-dropdown li:has(> ul) > a::after {
content: none;
}
}Using it on a page with a global CSS reset? It doesn't matter — the menu sets its own margins, padding and list styles.
Accessibility and browser support
What works without a single line of script, and what doesn't — honestly.
| Situation | Without JS | Details |
|---|---|---|
| Mouse | Works | Opens on hover, with a short close delay for slips. |
| Keyboard | Works | Every link is reachable with Tab and Shift+Tab. Each branch opens as you tab into it, so keyboard users pass every link of that branch. |
| Screen readers | Works | Announced as navigation with nested lists and a current page (aria-current="page"). |
| Phones | Works | In narrow space, all levels are listed open — nothing depends on hover. |
| Tablets, wide layout | Test it | Most touch browsers open the submenu on the first tap and follow the link on the second. Test on the devices your visitors use. |
| Esc to close | Needs JS | CSS can't react to key presses. Moving focus away closes the menu. |
| Older browsers | Degrades | Without :has() only the arrows are missing. Without container queries the desktop layout is used everywhere. |
- Closing with Esc. A few lines of script can add it on top — the menu keeps working if the script fails.
- Very large menus. If keyboard users would tab through dozens of links, let Esc skip a branch with a small script (shown on the mega menu page), or use parent buttons with
aria-expandedthat open and close each branch. - Menus built from live data. If items come from an API, render them on the server instead — that keeps them crawlable.
Why a CSS dropdown is better for SEO
Search engines discover pages by following links in the HTML. In this menu, all links — including the level-3 links that are hidden until you hover — are in the page source from the first byte. A crawler doesn't have to hover, click or run anything to find them.
Menus that are built or filled by JavaScript are a gamble. Google renders JavaScript, but in a second step and not always completely. Baidu and many AI crawlers mostly work with the raw HTML they download. If your submenu links only appear after a script runs, those engines may never see the pages behind them — and deep pages lose the internal links that help them rank.
Hiding submenu links with CSS is not a problem. It's standard navigation and is treated as such. Keep the menu to the pages that matter, though: a menu with hundreds of links dilutes them.
Questions about CSS-only dropdown menus
Can a dropdown menu work without JavaScript?
Yes. Nested lists with :hover and :focus-within open submenus for mouse and keyboard users in all current browsers. The demo on this page uses no JavaScript at all.
Is a CSS-only dropdown menu accessible?
It covers mouse, keyboard and screen-reader use, because every item is a normal link in a nested list. Two things need script: closing with Esc, and parent buttons with aria-expanded for very large menus. Add them as an enhancement on top — the CSS version keeps working if the script fails.
How does it work on phones and touch screens?
On narrow screens this menu shows every level open, so nothing depends on hover. For a collapsible mobile menu, combine it with the hamburger menu without JavaScript.
Does Google index links in a hidden dropdown menu?
Yes — as long as the links are in the HTML. Links hidden with CSS are followed like any other navigation link. Links that only appear after JavaScript runs are the ones at risk, especially for Baidu and AI crawlers.
Why use visibility instead of display: none?
Both remove hidden links from the tab order. visibility can be combined with opacity and a transition delay, which gives the fade-in and the close delay. display: none works too if you don't need animation.
This FAQ is itself built without JavaScript — see the accordion pattern.
Can Baidu see all of your pages?
We audit websites for exactly this: menus, links and content that only appear after JavaScript runs. Then we fix them — and run Baidu SEO and Chinese GEO for Western companies entering China.
CSS-First UI Patterns are written and maintained by Jademond Digital, a Chinese digital marketing agency founded in Guangzhou in 2012. We build and audit websites for Baidu, Google and AI search — which is why we care what works without JavaScript. About us →