Editor’s note: This post was updated on 12 May 2023 to include additional dropdown menu animation effects and to address limitations faced with CSS-only approaches.
Dropdown menus are a type of design pattern that allows us to hide or reveal a dropdown box, making it easy for users to navigate websites and applications. With a simple click or hover, this type of menu will gracefully display predefined content or a list of items.
In this guide, we’ll explore the art of creating captivating dropdown menus that can seamlessly integrate into a navigation bar or any section of a web project. We’ll begin with CSS-only solutions, the :focus
and :focus-within
pseudo-classes and a checkbox hack. Then, we’ll delve into the realm of JavaScript, addressing the limitations faced with CSS-only approaches.
We’ll discuss building dropdown menus for both larger and smaller screens, as well as using semantic HTML elements and ARIA attributes to assist screen readers. By the end of this tutorial, you’ll be able to create amazing, responsive, accessible dropdown menus for both keyboard and mouse users.
Let’s get started!
Jump ahead:
- Creating a dropdown menu with the CSS
:focus
pseudo-class - Creating a dropdown menu with the CSS
:focus-within
pseudo-class - Toggling the dropdown using a CSS checkbox hack
- Creating a dropdown menu with CSS + JavaScript
- Enhancing dropdown menus for screen readers
- Adding a dropdown menu to a Navbar
- Mobile-first dropdown menu design with CSS
- Designing dropdown menus for larger screens with CSS
Creating a dropdown menu with the CSS :focus
pseudo-class
To start, let’s use the CSS :focus
selector to create a dropdown menu that looks like this:
See the Pen
CSS dropdown menu using :focus by ibaslogic (@ibaslogic)
on CodePen.
Building an accessible dropdown menu begins with using semantic and interactive HTML5 elements where appropriate.
The following code snippet establishes a simple menu button along with its associated dropdown items:
<div class="dropdown"> <button class="dropdown-btn"> <span>Framework</span> <span class="arrow"></span> </button> <ul class="dropdown-content"> <li><a href="#">React</a></li> <li><a href="#">Angular</a></li> <li><a href="#">Vue</a></li> <li><a href="#">Svelte</a></li> </ul> </div>
The <button>
element is an interactive component that we’ll use to initiate the opening of the dropdown. To ensure accessibility, we use an unordered list for the dropdown items; this enables screen readers to identify the number of links within the dropdown.
Now, let’s apply the following base CSS styling to the markup:
/* Dropdown styles */ .dropdown { max-width: 13em; margin: 80px auto 0; position: relative; width: 100%; } .dropdown-btn { background: #1d1f24; font-size: 18px; width: 100%; border: none; color: #fff; display: flex; justify-content: space-between; align-items: center; padding: 0.7em 0.5em; border-radius: 0.5em; cursor: pointer; } .arrow { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #fff; transition: transform ease-in-out 0.3s; } .dropdown-content { list-style: none; position: absolute; top: 3.2em; width: 100%; } .dropdown-content li { background: #2f3238; border-radius: 0.5em; } .dropdown-content li:hover { background: #1d1f24; } .dropdown-content li a { display: block; padding: 0.7em 0.5em; color: #fff; margin: 0.1em 0; text-decoration: none; }
The styling rules are configured to position the dropdown box below the menu button. This is achieved by applying a position: relative
property to the container element and a position: absolute
to the actual dropdown.
We also implement various other CSS styles to enhance the overall aesthetics of the dropdown. The final appearance should resemble the following:
Interacting with the dropdown
Next, let’s hide the dropdown until the menu button receives a focus due to the user’s action — either by a click, a tap, or a press of the keyboard’s tab key.
Let’s update the styles with the following:
.dropdown-content { /* ... */ visibility: hidden; } .dropdown-btn:focus + .dropdown-content { visibility: visible; } .dropdown-btn:focus > .arrow { transform: rotate(180deg); }
Here we’ve added a style rule that causes the dropdown indicator to rotate when the button is clicked.
Adding a sliding effect transition to the dropdown
Now, let’s add a smooth sliding effect when opening or closing the dropdown menu. This is achieved by applying the following rules:
.dropdown-content { /* ... */ overflow: hidden; } .dropdown-content li { /* ... */ position: relative; left: 100%; transition: 0.5s; } .dropdown-btn:focus + .dropdown-content li { left: 0; }
Applying a position: relative;
and a left
property to the dropdown items causes them to shift toward the right from their default position. Subsequently, when the button receives focus, they’ll smoothly transition back to their original position.
Specifying a delay for dropdown items
To enhance the visual flow of the dropdown menu, let’s add a delay when the menu items slide in and out.
To implement this, we’ll update the menu items to include a style
attribute with a custom property:
<div class="dropdown"> <!-- ... --> <ul class="dropdown-content"> <li style="--delay: 1;"><a href="#">React</a></li> <li style="--delay: 2;"><a href="#">Angular</a></li> <li style="--delay: 3;"><a href="#">Vue</a></li> <li style="--delay: 4;"><a href="#">Svelte</a></li> </ul> </div>
In the CSS stylesheet, we can access the CSS variables and apply a delay using the transition-delay
property, like so:
.dropdown-content li { /* ... */ transition-delay: calc(60ms * var(--delay)); }
Creating a dropdown menu with the CSS :focus-within
pseudo-class
If we carefully observe the dropdown implementation with the :focus
pseudo-class, we notice that it is only applied to the focused element — in this case, the menu button. Whenever the focus shifts to any other element, including the dropdown items, the dropdown closes.
To provide a more user-friendly experience, we‘d like the dropdown to remain open as long as we click within the active menu area. This is where the :focus-within
pseudo-class comes into play.
By applying :focus-within
to an element, the associated style rules will apply when any of its child elements, or the element itself, receives focus.
In this case, we’ll apply the :focus-within
to the .dropdown
container class. This way, when child elements such as the button or the dropdown items receive focus, the corresponding rules will be applied.
Let’s replace :focus
with :focus-within
in our CSS, resulting in the following:
/* .dropdown-btn:focus + .dropdown-content li { left: 0; } */ .dropdown:focus-within .dropdown-content li { left: 0; } /* .dropdown-btn:focus + .dropdown-content { visibility: visible; } */ .dropdown:focus-within .dropdown-content { visibility: visible; } /* .dropdown-btn:focus > .arrow { transform: rotate(180deg); } */ .dropdown:focus-within .dropdown-btn > .arrow { transform: rotate(180deg); }
If we test the dropdown, it should function correctly.
Accessibility support for the Safari browser
By default, using the tab key for navigation is disabled in Safari. We can enable it by opening our Safari Preferences by opening the Advanced settings and checking the “Press Tab to highlight each item on a webpage” checkbox in the Accessibility section.
Sometimes, clicking on the menu button may not result in the dropdown displaying in Safari. An element on Safari will get a :focus-within
if it has a tabindex="0"
. To address the issue, let’s add a tabindex
to the element on which we applied the pseudo-class:
<div class="dropdown" tabindex="0"> <button class="dropdown-btn"> <!-- ... --> </button> <ul class="dropdown-content"> <!-- ... --> </ul> </div>
The dropdown should now work as expected:
See the Pen
Using CSS :focus-within by ibaslogic (@ibaslogic)
on CodePen.
Toggling the dropdown using a CSS checkbox hack
With the previous methods, the dropdown could only be closed when the button or dropdown items lost focus. However, by utilizing CSS selectors cleverly, we can take advantage of the checked
state of an HTML checkbox to toggle the dropdown.
Based on our HTML structure, let’s replace the button element with an input checkbox:
<div class="dropdown"> <input type="checkbox" id="dropdown" /> <label for="dropdown" class="dropdown-btn"> <span>Framework</span> <span class="arrow"></span> </label> <ul class="dropdown-content"> <li><a href="#">React</a></li> <li><a href="#">Angular</a></li> <li><a href="#">Vue</a></li> <li><a href="#">Svelte</a></li> </ul> </div>
Then, in addition to the base styles, let’s add the following CSS:
input[type="checkbox"] { opacity: 0; position: absolute; } input[type="checkbox"]:focus + label { box-shadow: 0 0 20px rgb(83, 83, 83); }
Here, we use opacity: 0;
to hide the input checkbox instead of a display: none;
property and also add styles to ensure that the button receives a focus when a user interacts with it.
Now, let’s replace the style rules for :focus
or :focus-within
with the :checked
CSS pseudo-selector:
/* .dropdown-btn:focus + .dropdown-content { visibility: visible; } */ input[type="checkbox"]:checked ~ .dropdown-content { visibility: visible; } /* .dropdown-btn:focus > .arrow { transform: rotate(180deg); } */ input[type="checkbox"]:checked + label > .arrow { transform: rotate(180deg); }
After that, we’ll modify the style rules on the li
element and remove the sliding effect transition:
.dropdown-content li { background: #2f3238; border-radius: 0.5em; /* position: relative; left: 100%; transition: 0.5s; transition-delay: calc(60ms * var(--delay)); */ } /* .dropdown-btn:focus + .dropdown-content li { left: 0; } */
Now let’s implement another transition effect.
Adding a transform effect
Let’s add a smooth transition effect that repositions the dropdown vertically. We’ll update the styles for .dropdown-content
before and after the dropdown is active:
.dropdown-content { /* ... */ transform: translateY(-1em); transition: transform ease 0.3s; } input[type="checkbox"]:checked ~ .dropdown-content { visibility: visible; transform: translateY(0); }
The dropdown should now behave as expected:
See the Pen
Toggle with checkbox (Hack) by ibaslogic (@ibaslogic)
on CodePen.
Creating dropdown menus with CSS + JavaScript
Up until now, we’ve used CSS only to create dropdown menus. Now, let’s use JavaScript to toggle the dropdown instead of using the checkbox hack. We’ll begin by making modifications to the CSS:
/* .dropdown-btn:focus + .dropdown-content li { left: 0; } */ .dropdown-content.menu-open li { left: 0; } /* .dropdown-btn:focus + .dropdown-content { visibility: visible; } */ .dropdown-content.menu-open { visibility: visible; } /* .dropdown-btn:focus > .arrow { transform: rotate(180deg); } */ .arrow.arrow-rotate { transform: rotate(180deg); }
In this approach, we move away from using the CSS pseudo-classes and instead employ custom class names that we will add to the target elements using JavaScript.
The JavaScript code snippet below demonstrates the process. It begins by selecting the button, caret, and dropdown elements. Then, it listens for a click
event on the button and dynamically adds classes to the caret and dropdown elements:
const dropdownBtn = document.querySelector(".dropdown-btn"); const dropdownCaret = document.querySelector(".arrow"); const dropdownContent = document.querySelector(".dropdown-content"); // add click event to dropdown button dropdownBtn.addEventListener("click", () => { // add rotate to caret element dropdownCaret.classList.toggle("arrow-rotate"); // add open styles to menu element dropdownContent.classList.toggle("menu-open"); });
Let’s test the dropdown and ensure it works! See the project on CodePen:
See the Pen
Toggle with JavaScript by ibaslogic (@ibaslogic)
on CodePen.
Enhancing dropdown menus for screen readers
To improve accessibility for screen reader users, let’s add ARIA attributes that effectively convey the behavior, functionality, and state of the dropdown. Here are the ARIA attributes we will implement:
aria-label
: Add this attribute to interactive elements, like the menu button, to provide a clear description of its purpose; this attribute allows screen reader software to announce the control correctlyaria-haspopup
androle
: Apply these attributes to the menu button and dropdown, respectively, to inform the screen readers that there is a popup. Both must match the type of dropdown – in our case,menu
aria-controls
: Maps the controlling element to the expanded widget; we’ll set theid
of the expanded widget to the value of thearia-controls
attribute on the controlling elementaria-expanded
: Indicates whether the dropdown is currently hidden or visible; this attribute’s value should toggle betweentrue
andfalse
based on the state of the dropdown. We’ll dynamically update the value using JavaScript to reflect the current state of the dropdown
Let’s update the HTML markup to include the ARIA attributes:
<div class="dropdown"> <button class="dropdown-btn" aria-label="menu button" aria-haspopup="menu" aria-expanded="false" aria-controls="dropdown-menu" > <!-- ... --> </button> <ul class="dropdown-content" role="menu" id="dropdown-menu"> <!-- ... --> </ul> </div>
Then, with JavaScript, let’s dynamically update the aria-expanded
attribute based on the current state:
dropdownBtn.addEventListener("click", () => { //... dropdownBtn.setAttribute( "aria-expanded", dropdownBtn.getAttribute("aria-expanded") === "true" ? "false" : "true" ); });
If we toggle the button, the attribute should also update:
Adding a dropdown menu to a Navbar
Utilizing the knowledge we have acquired thus far, let’s integrate a dropdown menu within a navigation bar using CSS.
By the end of the process, we’ll have successfully developed a responsive CSS dropdown menu:
See the Pen
Logo by ibaslogic (@ibaslogic)
on CodePen.
We employed a CSS-only solution by using the input checkbox to toggle the main dropdown on smaller screens and the :focus-within
pseudo-class to activate the Services
dropdown.
Navbar and dropdown menu markup
The below code establishes a basic navigation structure comprising three primary menu items: Home
, Services
, and About
. The Services
item includes a dropdown feature with three nested ul
submenu items:
<header> <div class="header-content"> <a href="#" class="logo">Logo</a> <input type="checkbox" id="hamburger"> <label for="hamburger" class="hamburger"> <span></span> </label> <nav> <ul class="menus"> <li style="--delay: 1;"><a href="#">Home</a></li> <li style="--delay: 2;" tabindex="0"> <button> <span>Services</span> <span class="arrow"></span> </button> <ul class="dropdown"> <li><a href="#">Web design</a></li> <li><a href="#">Web dev</a></li> <li><a href="#">Web</a></li> </ul> </li> <li style="--delay: 3;"><a href="#">About</a></li> </ul> </nav> </div> </header>
The output should look like this:
Mobile-first dropdown menu design with CSS
We’ll start by styling the navigation for smaller screens. In the CSS file, we’ll add the following styles:
header { position: relative; background: #1d1f24; color: #fff; } .header-content { align-items: center; max-width: 1200px; margin: 0 auto; padding: 10px 20px; } .logo { text-decoration: none; font-size: 25px; color: inherit; margin-right: 20px; } input[type="checkbox"] { opacity: 0; position: absolute; right: 0; } .hamburger { padding: 23px 20px; position: absolute; cursor: pointer; right: 0; top: 0; } .hamburger span { width: 20px; height: 3px; display: block; background: #fff; position: relative; } .hamburger span::before, .hamburger span::after { content: ""; position: absolute; display: block; background: inherit; width: inherit; height: inherit; } .hamburger span::before { top: 8px; } .hamburger span::after { bottom: 8px; } input[type="checkbox"]:focus + .hamburger { box-shadow: 0 0 20px rgba(0,0,0,.45); } ul { list-style: none; } ul li { font-size: 18px; } ul li a { display: block; text-decoration: none; } ul li a, ul li button { padding: 0.7rem 1rem; text-align: left; color: inherit; } .menus { position: absolute; top: 3.2rem; left: 0; right: 0; background: #2f3238; } .dropdown { padding: 2px 1.5rem; } button { font-size: inherit; border: none; background-color: transparent; cursor: pointer; width: 100%; display: flex; align-items: center; gap: 1em; } .arrow { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #fff; transition: transform ease-in-out 0.3s; } /* base styles */ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: "Segoe UI", sans-serif; color: #333; }
Now we should have a dropdown menu that looks like this:
What is happening in the CSS?
As expected, using the CSS position: absolute;
on the dropdown element, places it relative to the navigation bar, as seen in the image above.
We then used the ::before
and ::after
pseudo-classes to transform the input checkbox into a custom hamburger menu to activate a dropdown on smaller screens. Subsequently, several other CSS styles were applied to elevate the overall visual appeal.
Interacting with the dropdown blocks
To hide the main dropdown, we’ll use the CSS visibility
property and specify height: 0
for the Services
dropdown:
.menus { /* ... */ visibility: hidden; transform: translateY(-1em); transition: transform ease 0.5s; } .dropdown { /* ... */ height: 0; overflow: hidden; transition: height ease 0.2s; }
Here, we’ve included additional CSS properties to ensure a seamless transition effect.
Next, we’ll use the :checked
CSS pseudo-selector and the :focus-within
pseudo-class to toggle the main dropdown and activate the Services
dropdown, respectively:
input[type="checkbox"]:checked ~ nav .menus { visibility: visible; transform: translateY(0); } li:focus-within .dropdown { height: 135px; } li:focus-within .arrow { transform: rotate(180deg); }
Adding a sliding effect transition to dropdown items
Similar to our previous approach, let’s add animation using the following style rules:
.menus li { position: relative; left: 100%; transition: 0.2s; transition-delay: calc(60ms * var(--delay)); } input[type="checkbox"]:checked ~ nav > ul li { left: 0; }
The dropdown should now behave like so:
Designing dropdown menus for larger screens with CSS
We’ll use the CSS media queries and define the style rules for a screen width of 640px
and above:
/* MEDIA QUERIES */ @media (min-width: 640px) { .header-content { display: flex; } .menus { position: static; visibility: visible; background: transparent; display: flex; transform: initial; } .menus li { font-size: 14px; left: auto; } .hamburger, input[type="checkbox"] { display: none; } ul li a:hover, ul li button:hover { background-color: #2f3238; } .dropdown { position: absolute; top: 48px; right: 0; left: auto; z-index: 99; min-width: 10rem; padding: 0; background-color: #1d1f24; border-radius: 0 0 0.5rem 0.5rem; } }
In this CSS code, we added a display: flex;
to the header content to position the logo and navigation menus side-by-side. Then, we added a position: static;
declaration on the navigation content to override the absolute positioning we used for mobile design and place the nav items in the normal flow of the page.
Conclusion
In this article, we’ve demonstrated how to create an impressive dropdown menu using CSS. Our sample menu is designed to be accessible to users who rely on keyboards, mouse, and screen readers. Additionally, we explored how to enhance the dropdown functionality using JavaScript.
Dropdown menus are a common design pattern in modern apps. To learn more about them, you can look at building a multilevel dropdown menu in React.
If you have questions or contributions, please share your thoughts in the comment section. Finally, endeavor to share this lesson around the web.
Is your frontend hogging your users' CPU?
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — Start monitoring for free.