惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
O
OpenAI News
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
云风的 BLOG
云风的 BLOG
罗磊的独立博客
S
SegmentFault 最新的问题
The Register - Security
The Register - Security
Hugging Face - Blog
Hugging Face - Blog
D
DataBreaches.Net
U
Unit 42
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
雷峰网
雷峰网
V
Visual Studio Blog
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
博客园 - 【当耐特】
G
Google Developers Blog
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
I
InfoQ
Martin Fowler
Martin Fowler
F
Fortinet All Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
AI
AI
Google Online Security Blog
Google Online Security Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - Franky
Blog — PlanetScale
Blog — PlanetScale
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org

CSS Wizardry

Front-End’s Missing Metric: The TBT Window Meet Your Users Where They Are with Obs.js Better Browser Caching with No-Vary-Search font-family Doesn’t Fall Back the Way You Think What Is CSS Containment and How Can I Use It? When All You Can Do Is All or Nothing, Do Nothing Obs.js: Context-Aware Web Performance for Everyone Low- and Mid-Tier Mobile for the Real World (2025) The Fastest Site in the Tour de France Making Sense of the Performance Extensibility API Why Do We Have a Cache-Control Request Header? HTML Is Not a Programming Language… Build for the Web, Build on the Web, Build with the Web Licensing Code on CSS Wizardry A Layered Approach to Speculation Rules Designing (and Evolving) a New Web Performance Score Core Web Vitals Colours The Ultimate Contract Templates for Tech Consultants: Protect Your Business and Get Paid Optimising for High Latency Environments Cache Grab: How Much Are You Leaving on the Table? blocking=render: Why would you do that?! Correctly Configure (Pre) Connections The Three Cs: 🤝 Concatenate, 🗜️ Compress, 🗳️ Cache What Is the Maximum max-age? How to Clear Cache and Cookies on a Customer’s Device The Ultimate Low-Quality Image Placeholder Technique Core Web Vitals for Search Engine Optimisation: What Do We Need to Know? The HTTP/1-liness of HTTP/2 In Defence of DOM­Content­Loaded Site-Speed Topography Remapped Why Not document.write()? Speeding Up Async Snippets Critical CSS? Not So Fast! Measure What You Impact, Not What You Influence Optimising Largest Contentful Paint Measuring Web Performance in Mobile Safari Site-Speed Topography Speed Up Google Fonts Real-World Effectiveness of Brotli Performance Budgets, Pragmatically Lazy Pre-Browsing with Prefetch Making Cloud.typography Fast(er) Time to First Byte: What It Is and How to Improve It Self-Host Your Static Assets Tips for Technical Interviews Cache-Control for Civilians Bandwidth or Latency: When to Optimise for Which ITCSS × Skillshare What If? CSS and Network Performance The Three Types of Performance Testing Getting to Know a Legacy Codebase Image Inconsistencies: How and When Browsers Download Images Identifying, Auditing, and Discussing Third Parties My Digital Music Setup Measuring the Hard-to-Measure Finding Dead CSS The Fallacies of Distributed Computing (Applied to Front-End Performance) Ten Years Old Relative Requirements Airplanes and Ashtrays Performance and Resilience: Stress-Testing Third Parties Refactoring Tunnels Little Things I Like to Do with Git Writing Tidy Code Configuring Git and Vim Base64 Encoding & Performance, Part 2: Gathering Data Base64 Encoding & Performance, Part 1: What’s Up with Base64? Code Smells in CSS Revisited Typography for Developers Moving CSS Wizardry onto HTTPS and HTTP/2 Ack for CSS Developers A New Year, a New Focus Preparing Vim for Apple’s Touch Bar Choosing the Correct Average CSS Shorthand Syntax Considered an Anti-Pattern CSS Wizardry Newsletter Nesting Your BEM? Improving Perceived Performance with Multiple Background Images Continue Normalising Your CSS Pure CSS Content Filter Pragmatic, Practical, and Progressive Theming with Custom Properties Refactoring CSS: The Three I’s Speaker’s Checklist: Before and After Your Talk Improving Your CSS with Parker The Importance of !important: Forcing Immutability in CSS Mixins Better for Performance Managing Typography on Large Apps White October Events Workshop Partnership BEMIT: Taking the BEM Naming Convention a Step Further Travelling Like You Want to, When You Have To Contextual Styling: UI Components, Nesting, and Implementation Detail Subtleties with Self-Chained Classes Cyclomatic Complexity: Logic in CSS Immutable CSS Can CSS Be Too Modular? More Transparent UI Code with Namespaces When to use @extend; when to use a mixin The Specificity Graph CSS Wizardry Ltd.: Year 1 in review
Creating a pure CSS dropdown menu
Harry Roberts · 2011-02-17 · via CSS Wizardry

Written by on CSS Wizardry.

Table of Contents

Independent writing is brought to you via my wonderful Supporters.

  1. Demo
  2. The concept
    1. Featured case study: NHS
  3. The markup
  4. The CSS
    1. The nested lists
  5. Final word

In redeveloping the Venturelab site we became increasingly aware that there was a lot of content that needed navigating extremely simply and fairly rapidly. We have so much to say and such a lot of content that the navigation of the site needed to be even more dynamic and encompassing than normal.

Demo

This article has been ported from the now-defunct Venturelab Devblog, where I had originally authored it.

Need Some Help?

I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.

Each page features a sub-navigation area, which links to all the other pages within that section of the website. This is great, and works perfectly, but in order to get to, say, the FAQs page from the home page, you’d first have to go to the about page, then on to the FAQs from there. This is by no means unacceptable, but we like to go that extra step at Venturelab…

I was looking at the main menu of the site when inspiration struck. Something as common and simple as a series of dropdown menus under each meta menu item would improve the navigability and usability of the site massively. Also, they are incredibly simple to create, and here’s where I teach you how…

The concept

What a dropdown menu provides is a hierarchical overview of the subsections contained within the menu item that spawned it. Basically, it lists all the subsections within a section of a site when you hover your mouse cursor over it.

Featured case study: NHS

How I helped the NHS rapidly build a brand new product.

Read case study…

They are extremely useful in showing what a section of a site contains, and allowing you to access it from anyway else in that site, whether that be the parent page of that subsection, or a page in a different section altogether.

The markup

A lot of dropdown menus rely on bulky, extraneous markup and Javascript to work, ours will use only the cleanest HTML and some lean CSS, with some lovely progressive CSS3 for good measure.

<ul class="nav">
    <li>
        <a href="#">Home</a>
    </li>

    <li>
        <a href="#">About</a>
        <ul>
            <li><a href="#">The product</a></li>

            <li><a href="#">Meet the team</a></li>
        </ul>
    </li>
    <li>
        <a href="#">Services</a>

        <ul>
            <li><a href="#">Sevice one</a></li>
            <li><a href="#">Sevice two</a></li>

            <li><a href="#">Sevice three</a></li>
            <li><a href="#">Sevice four</a></li>
        </ul>

    </li>
    <li>
        <a href="#">Product</a>
        <ul>
            <li><a href="#">Small product (one)</a></li>

            <li><a href="#">Small product (two)</a></li>
            <li><a href="#">Small product (three)</a></li>
            <li><a href="#">Small product (four)</a></li>

            <li><a href="#">Big product (five)</a></li>
            <li><a href="#">Big product (six)</a></li>
            <li><a href="#">Big product (seven)</a></li>

            <li><a href="#">Big product (eight)</a></li>
            <li><a href="#">Enormous product (nine)</a></li>
            <li><a href="#">Enormous product (ten)</a></li>

            <li><a href="#">Enormous product (eleven)</a></li>
        </ul>
    </li>
    <li>
        <a href="#">Contact</a>

        <ul>
            <li><a href="#">Out-of-hours</a></li>
            <li><a href="#">Directions</a></li>

        </ul>
    </li>
</ul>

As you can see here the markup is simply a series of nested <ul>s. No verbose IDs/classes, no <div>s, just rich, semantic code.

The .nav <ul> contains a series of <li>s, and any that require a dropdown then contain another <ul>. Notice the dropdown <ul>s have no classes on them–this is because we use the cascade to style these, keeping our markup even cleaner.

The CSS

This is where the magic happens–we use CSS to transform a series of nested <ul>s into a smooth, easy to use, neat and self-contained dropdown menu.

/* BE SURE TO INCLUDE THE CSS RESET FOUND IN THE DEMO PAGE'S CSS */

/*------------------------------------*\
    NAV
\*------------------------------------*/
.nav {
    list-style: none;
    font-weight: bold;
    margin-bottom: 10px;
    float: left; /* Clear floats */
    width: 100%;
    /* Bring the nav above everything else--uncomment if needed.
    position: relative;
    z-index: 5;
    */
}
.nav li {
    float: left;
    margin-right: 10px;
    position: relative;
}
.nav a {
    display: block;
    padding: 5px;
    color: #fff;
    background-color: #333;
    text-decoration: none;
}
.nav a:hover {
    color: #fff;
    background-color: #6b0c36;
    text-decoration: underline;
}

/*--- DROPDOWN ---*/
.nav ul {
    background-color: #fff; /* Adding a background makes the dropdown work properly in IE7+. Make this as close to your page's background as possible (i.e. white page == white background). */
    background: rgba(255,255,255,0); /* But! Let's make the background fully transparent where we can, we don't actually want to see it if we can help it... */
    list-style: none;
    position: absolute;
    left: -9999px; /* Hide off-screen when not needed (this is more accessible than display: none;) */
}
.nav ul li {
    padding-top: 1px; /* Introducing a padding between the li and the a give the illusion spaced items */
    float: none;
}
.nav ul a {
    white-space: nowrap; /* Stop text wrapping and creating multi-line dropdown items */
}
.nav li:hover ul { /* Display the dropdown on hover */
    left: 0; /* Bring back on-screen when needed */
}
.nav li:hover a { /* These create persistent hover states, meaning the top-most link stays 'hovered' even when your cursor has moved down the list. */
    background-color: #6b0c36;
    text-decoration: underline;
}
.nav li:hover ul a { /* The persistent hover state does however create a global style for links even before they're hovered. Here we undo these effects. */
    text-decoration: none;
}
.nav li:hover ul li a:hover { /* Here we define the most explicit hover states--what happens when you hover each individual link. */
    background-color: #333;
}

Just a regular horizontal navigation menu…

Right, let’s now break that down… The first section is fairly self explanatory–here we are just setting up a regular horizontal navigation menu, the same as any other. However, notice that selectors such as .nav li and .nav li a will select all list items and links in the dropdowns too. Here we’re using the cascade sensibly.

One thing of note however is applying position: relative; to the list items, this allows us to use position: absolute; on the nested <ul>s later on.

The nested lists

.nav ul {
    background-color: #fff; /* Adding a background makes the dropdown work properly in IE7+. Make this as close to your page's background as possible (i.e. white page == white background). */
    background: rgba(255,255,255,0); /* But! Let's make the background fully transparent where we can, we don't actually want to see it if we can help it... */
    list-style: none;
    position: absolute;
    left: -9999px; /* Hide off-screen when not needed (this is more accessible than display: none;) */
}

Here we have the CSS that controls the <ul>s nested within the top level list items. Obviously we need to remove bullets with list-style: none;, then position: absolute; to position the dropdown above the list item that holds it.

A better, more accessible solution than display: none;

The next line however is a point of interest. Usually, most people would use display: none; to hide the dropdown while it’s not being used, but due to a lot of screenreaders ignoring anything with display: none; applied, this is very inaccessible. What we do instead is take advantage of the fact the <ul> is absolutely positioned and just position it -9999px off screen when not in use.

Next up we declare opacity: 0; for the hidden <ul> and then a Webkit only declaration which will smoothly fade the <ul> in from fully transparent when hovered.

.nav ul li {
    padding-top: 1px; /* Introducing a padding between the li and the a give the illusion spaced items */
    float: none;
}
.nav ul a {
    white-space: nowrap; /* Stop text wrapping and creating multi-line dropdown items */
}
.nav li:hover ul { /* Display the dropdown on hover */
    left: 0; /* Bring back on-screen when needed */
}

Here we set up the default list item and link styles. Notice the padding-top: 1px; on the <li>. As all the colours etc are applied to the <a>, putting a 1px padding on the <li> in effect pushes the <a>–and therefore the colour–away from the edge of the list item, giving it the illusion that they are all separated. Interestingly, IE will not recognise the layout of the <li> when hovered, closing the dropdown again. To get round this, I added a 1×1px transparent gif image as a background. Also here we remove the floats applied earlier.

Next, on .nav ul a, we apply white-space: nowrap; to prevent items wrapping onto two lines, ensuring a consistent display.

And this is where the magic happens…

The final bit of code is the bit that actually makes the dropdown appear when the list item that contains it is hovered. Now, as the :hover pseudo-class only works on the <a> element in IE6, the dropdowns won’t work in that browser. That can be alleviated by using a variety of fixes. However, as dropdowns are progressive, then we’re okay with them not working. If you do however want to get this working in IE6 then my favoured solutions is by using behaviours.

.nav li:hover a { /* These create persistent hover states, meaning the top-most link stays 'hovered' even when your cursor has moved down the list. */
    background-color: #6b0c36;
    text-decoration: underline;
}

This gets tricky, but it should make sense.

This block of code here is where the hover styles come in, there’s a bit of nifty code in there which controls what we’ll call ‘persisting hover states’ on the top level item even when the user is hovering the dropdown items…

.nav li:hover a is what allows you to give the top level link a persisting hover state when hovering its ‘children’. This works by styling every link inside a list-item when that list-item is hovered. This bit gets a bit tricky but bear with me:

  • The dropdown <ul> sits inside an <li>.
  • If you are hovering over a link (<a>) in a dropdown (<ul>) then you are also, at the same time, still hovering the top level list-item (<li>) as you are hovering content inside it.
  • Because you are technically still hovering the top level list-item, the .nav li:hover a remains true, leaving a persisting hover style on the top level list-item’s <a> so…
  • …by hovering a dropdown item you are still hovering the top level list-item which means the cascade still styles all links in that list-item. Phew!
.nav li:hover ul a { /* The persistent hover state does however create a global style for links even before they're hovered. Here we undo these effects. */
    text-decoration: none;
}

Here we override certain aspects of the persisting hover state so that the dropdowns differ from the top level link. In this case we merely decide not to underline them.

We also add a touch of Webkit goodness, telling the links to transform. -webkit-transition: -webkit-transform 0.075s linear; tells Webkit to animate the -webkit-transform we apply later on in the code over 0.075 seconds with no fade-in/out. Look out for the initiation of this in the next (and final) block of CSS.

.nav li:hover ul li a:hover { /* Here we define the most explicit hover states--what happens when you hover each individual link. */
    background-color: #333;
}

Final word

View demo…

So, there you have it. A simple concept pulled off with some very lean markup and some clever CSS and progressive enhancement. It’s totally accessible, the markup is semantic and sensible and it relies on no weighty Javascript libraries to work.

Hopefully my write-up makes sense. but if anything is unclear leave a comment and one of us in the dev team will try and set you right. Or, you could just copy/paste the code and hack it apart for yourselves.