loading...
Cover image for Building a pure CSS menu with nested dropdowns

Building a pure CSS menu with nested dropdowns

felipperegazio profile image Felippe Regazio ใƒป8 min read

Well, our mission is to build a dropdown menu with nested lists using <ul> elements, which we can nest as many items as needed, and they will behave like a dropdown structure. So lets go.

Here is the codepen with the result:

As the dropdown grows horizontally, it will not be fully responsive. And also uses the hover as trigger, which must be a problem on mobo. You can change the way it grows, or change it to a hamburger sidebar or whatever, thats a subject for another post. Anyway, this code still saves a lot of javascript.

For now, we gonna focus on simply build the dropdown structure. For this post you will need just a basic knowledge on html and css/scss.

Initial Structure

Our initial html structure will be used to create a navbar to hold our menu, and must be something like this:

<div class="menu">
  <ul> 
    <li class="link">
      <a href="">This is a link</a>
    </li>
    <li>
      This will open a dropdown soon
    </li>
  </ul>
</div>

You can add a dropdown menu anywhere. You shouldn't, but you can. Here, we will add on our navbar. So, the "menu" div will be fixed on top of the page. You can use a <nav> tag as well. Here is the css to fix the div on top.

.menu {
  // define the height of the menu
  --menu-height: 40px;
  // holder and ul general style
  box-sizing: border-box;
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
}

If you are not familiar with css vars, you can read this post:
https://dev.to/sarah_chima/an-introduction-to-css-variables-cmj

The --menu-height is a css var which will define the height of our navbar. This will be useful to define its children position too.

Now that we have our fixed nav, its important to note that we have three scopes when targeting the ul's inside it:

  1. We can target all the ul's (entire scope). For the entire scope you must simply use: ".menu ul" as selector.

  2. We can target only the first ul (inner scope). This is the menu holder, and will be the first level of the menu. You can select it using: ".menu > ul", we'll talk more about it later.

  3. We can target only the nested ul's (nested scope). This will be our dropdowns. You can select 'em using: ".menu > ul li ul", this will select all the ul's inside list items which, by the way, will be placed inside the first ul (menu holder) of the fixed div.

So, first we will target all the ul's in order to add a common appearance for them all, avoiding code repetition.

Note that we style the <a> tags as well, this will normalize the appearance of all li's, even if they're a link or a dropdown.

.menu {
  // ...
  ul {
    list-style: none;
    padding: 16px;
    margin: 0;
    li, li a {
      opacity: .8;
      color: #ffffff;      
      cursor: pointer;
      transition: 200ms;
      text-decoration: none;
      white-space: nowrap;
      font-weight: 700;
      &:hover {
        opacity: 1;
      }
      a {
        display: flex;
        align-items: center;
        height: 100%;
        width: 100%;      
      }      
    }
  }
}

Now, all the ul's and li's tags inside the menu will inherit the style above. We also gonna need an arrow to mark the items on menu which haves a dropdown.

But in CSS terms, how we know which item has a dropdown and who dont?

We simple add a class to differ the li's. When we have just a <li>, its a dropdown. When we have a <li> with a ".link" class, its a link.

Knowing that, we can add the arrow to the li's like this:

.menu {
  // ...
  ul {
    // ...
    // lets put an arrow down 
    // to the li`s with dropdown
    li {
      padding-right: 36px;
      &::before {
        content: '';
        width: 0; 
        height: 0; 
        border-left: 5px solid transparent;
        border-right: 5px solid transparent;
        border-top: 5px solid #FFA500;
        position: absolute;
        right: 8px;
        top: 50%;
        transform: translateY(-50%);
      }
    }
    .link {
      // links dont need arrow
      &::before {
        padding-right: 0;
        display: none;
      }      
    }
  }
}

To understand how we created an arrow using only css, you can read this post on css-tricks: https://css-tricks.com/snippets/css/css-triangle/.

The menu

The first <ul> inside the ".menu" div will be the menu holder. We want it to be a horizontal list of items which can be a link or a dropdown. So, we have to place the list items side by side and hide its children:

.menu {
  // ...
  // the first ul inside the container
  // is the menu, so must be visible 
  // and have its own style
  > ul {
    display: flex;
    height: var(--menu-height);
    align-items: center;
    background-color: #000000;
    // the first ul elements can be a
    // link or an li with a nested ul. 
    // the nested ul will be a dropdown
    li {
      position: relative;
      margin: 0 8px;
      // the dropdown style
      ul {
        // THE DROPDOWN GOES HERE
      }
    }
  }
}

We must use the > operator to select the first ul element inside the menu div. We need it to make our first ul looks like a menu without mess with the inner ul's.

Note the comment "// THE DROPDOWN GOES HERE" on the code example. If you take a look on our html structure, the list items must hold a ul (dropdown). This comment shows exactly where we are targeting this dropdown in our CSS code.

But first, lets add it on the html

<div class="menu">
  <ul>
    <li class="link">
      <a href="">This is a link</a>
    </li>   
    <li>
      Now we have a dropdown
      <ul>
        <li class="link">
          <a href="">Sub A</a>
        </li>
        <li class="link">
          <a href="">Sub B</a>
        </li>        
      </ul>
    </li>
  </ul>
</div>

The Dropdown

The dropdown is every <ul> inside a list item on our menu holder, which is the first ul on the ".menu" div. We must tell it to the CSS. But first, lets understand how the CSS deals with it:

// the menu
.menu {
  // the menu holder with options
  > ul {
     // the menu items
     li {
        // the item dropdown
        ul {
        }
     }
  }
}

The li must show its <ul> child when hovered. So, lets tell the list items to do it. But before, we must tell the dropdowns how and where they must appear:

.menu {
  // ...
  > ul {
    // ...
    li {
      // the holder
      position: relative;
      margin: 0 8px;
      ul {
        // the dropdown
        visibility: hidden;
        opacity: 0;        
        padding: 0;
        min-width: 160px;
        background-color: #333;
        position: absolute;
        top: calc(var(--menu-height) + 5px);
        left: 50%;
        transform: translateX(-50%);
        transition: 200ms;
        transition-delay: 200ms;
        // the dropdown items style
        li {
          margin: 0;
          padding: 8px 16px;
          display: flex;
          align-items: center;
          justify-content: flex-start;
          height: 30px;
          padding-right: 40px;
          // lets put an arrow right
          // to the inner li`s with
          // dropdowns
          &::before {
            width: 0; 
            height: 0; 
            border-top: 5px solid transparent;
            border-bottom: 5px solid transparent;
            border-left: 5px solid #FFA500;
          }
          // every dropdown after the
          // first must open to the right
          ul {
            top: -2%;
            left: 100%;
            transform: translate(0)
          }
          &:hover {
            background-color: #000000;
          }
        }
      }
      // on hover an li (not an <a>)
      // must show its ul (dropdown)
      &:hover {
        > ul {
          opacity: 1;
          visibility: visible;
          transition-delay: 0ms;
        }
      }
    }
  }
}

This is a very long and important part. Lets understand it.

We are selecting the all the ul's inside a list item, and turning it into a dropdown. To achieve it, we must follow this steps:

  1. Start telling the CSS how to draw the nested ul and where to place it. At first, this ul starts invisible and will be showed when we hover its parent. When visible, the following code will in charge to put all on the right place:
// ...
position: absolute;
top: calc(var(--menu-height) + 5px);
left: 50%;
transform: translateX(-50%);
transition: 200ms;
transition-delay: 200ms;

The top will set the dropdown to be right after the navbar, thats why we setted the --menu-height var.

The left + transform will centralize the dropdown relative to its parent. The transition will make it smooth and the delay will give the cursor an interval to navigate between dropdowns.

  1. The list item which is holding the dropdown (ul) must be relative. So we can give an absolute position to the dropdown and make its position relative to its parent. On the code, this li has a flex style to centralize its content.

  2. One thing that can be tricky: We will override the position of the dropdown after the first level. Thats because after that, the dropdowns must tend to the right.

You can ignore the "left" selector on here if you want the dropdowns to tend to the bottom. But you cannot ignore the top property, or the your dropdowns will overlap.

This is the part of the code that gives the nested dropdowns its right position:

// every dropdown after the first level
// must pair the top with its parent, 
// and must tend to the right
ul {
  top: -2%;
  left: 100%;
  transform: translate(0)
}

And, finally, we gonna show the dropdown when hovering its parent:

// when hover an li (not an <a>)
// must show its ul (dropdown)
&:hover {
  > ul {
    opacity: 1;
    visibility: visible;
    transition-delay: 0ms;
  }
}

Done

At the end, you must have the following code:

.menu {
  // define the height of the menu
  --menu-height: 40px;
  // holder and ul general style
  box-sizing: border-box;
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  ul {
    list-style: none;
    padding: 16px;
    margin: 0;
    li, li a {
      opacity: .8;
      color: #ffffff;      
      cursor: pointer;
      transition: 200ms;
      text-decoration: none;
      white-space: nowrap;
      font-weight: 700;
      &:hover {
        opacity: 1;
      }
      a {
        display: flex;
        align-items: center;
        height: 100%;
        width: 100%;      
      }      
    }
    // lets put an arrow down 
    // to the li`s with dropdown
    li {
      padding-right: 36px;
      &::before {
        content: '';
        width: 0; 
        height: 0; 
        border-left: 5px solid transparent;
        border-right: 5px solid transparent;
        border-top: 5px solid #FFA500;
        position: absolute;
        right: 8px;
        top: 50%;
        transform: translateY(-50%);
      }
    }
    .link {
      // links dont need arrow
      &::before {
        padding-right: 0;
        display: none;
      }      
    }
  }
  // the first ul inside the container
  // is the menu, so must be visible 
  // and have its own style
  > ul {
    display: flex;
    height: var(--menu-height);
    align-items: center;
    background-color: #000000;
    // the first ul elements can be a
    // link or an li with a nested ul. 
    // the nested ul will be a dropdown
    li {
      position: relative;
      margin: 0 8px;
      // the dropdown style
      ul {
        visibility: hidden;
        opacity: 0;        
        padding: 0;
        min-width: 160px;
        background-color: #333;
        position: absolute;
        top: calc(var(--menu-height) + 5px);
        left: 50%;
        transform: translateX(-50%);
        transition: 200ms;
        transition-delay: 200ms;
        // the dropdown items style
        li {
          margin: 0;
          padding: 8px 16px;
          display: flex;
          align-items: center;
          justify-content: flex-start;
          height: 30px;
          padding-right: 40px;
          // lets put an arrow right
          // to the inner li`s with
          // dropdowns
          &::before {
            width: 0; 
            height: 0; 
            border-top: 5px solid transparent;
            border-bottom: 5px solid transparent;
            border-left: 5px solid #FFA500;
          }
          // every dropdown after the
          // first must open to the right
          ul {
            top: -2%;
            left: 100%;
            transform: translate(0)
          }
          &:hover {
            background-color: #000000;
          }
        }
      }
      // on hover an li (not an <a>)
      // must show its ul (dropdown)
      &:hover {
        > ul {
          opacity: 1;
          visibility: visible;
          transition-delay: 0ms;
        }
      }
    }
  }
}

And the css above will allow a html structure like that

<div class="menu">
   <ul>
     <li>
       Dropdown A
       <ul>
         <li class="link">
           <a href="">Im a link</a>
         </li>
         <li class="link">
           <a href="">Im a link</a>
         </li>
         <li>
           Nested dropdown
           <ul>
             <li class="link">
               <a href="">Im a link</a>
             </li>
             <li class="link">
               <a href="">Im a link</a>
             </li>
           </ul>
         </li>
       </ul>
     </li>
   </ul>
</div>

The ideia here its very common and simple, even if takes a time to understand. Every list item must show its child ul element on hover.

What givers some complexity its the fact that we must change the css rules for different levels of the dropdowns, because the elements changes its behavior.

The first ul level is the menu itself and its a horizontal bar. The second ul level is the horizontal and centralized dropdown line. The next ul level is the inner dropdowns, and they tend to the right, and so on.

You can use javascript to add a collision system to the dropdowns, change they way they grow, or change everything to an hamburger menu, etc. Its simple enough to scale very well.

I made my best trying to explain it, but i dont wanted to over explain something that, probably, its easiest to understand looking at the code. For that kind of thing, some examples, an overall explanation, and google as a tool must be enough to get the idea, i believe.

Hope that can helps the beginners after all.
And remember: the best teacher is the practice :)

Thanks for your time.
Thats all folks.

(Cover Photo by Jo Szczepanska on Unsplash)

Posted on by:

felipperegazio profile

Felippe Regazio

@felipperegazio

web developer - js, [s]css, node, php, python - intp, lifelong learner, father, skateboarder. a strange carbon-based lifeform.

Discussion

markdown guide
 

Very nice article and interesting. I would love to see a final version of this menu responsive and with some js on it.

 

thanks Bogdan, your comment can really be an incentive to write a second part.

 

just fyi: having an 'a' tag in a 'ul' tag is not valid HTML.

 

thats true, and fixed on the post. thanks!