In this CSS tutorial, you will learn how to create responsive, accessible dropdown menus with CSS for both keyboard and mouse users. Dropdown menus are a common design pattern in modern apps.
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!
Contents:
:focus
pseudo-class:focus-within
pseudo-class:focus
pseudo-classTo start, let’s use the CSS :focus
selector to create a dropdown menu that looks like this:
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:
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.
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.
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));
}
:focus-within
pseudo-classIf 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.
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:
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.
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:
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:
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
and role
: 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 the id
of the expanded widget to the value of the aria-controls
attribute on the controlling elementaria-expanded
: Indicates whether the dropdown is currently hidden or visible; this attribute’s value should toggle between true
and false
based on the state of the dropdown. We’ll dynamically update the value using JavaScript to reflect the current state of the dropdownLet’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:
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:
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.
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:
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:
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.
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);
}
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:
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.
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.
If you have questions or contributions, please share your thoughts in the comment section. Finally, endeavor to share this lesson around the web.
Source: https://blog.logrocket.com
#css