cts
cts

Reputation: 1084

How to use Flex with multiple levels of list item and maintain accessible content

I'm building a mega menu component using vanilla HTML, CSS & JS. The mega menu has 3 levels of content, with the first being in the direction of row along the banner.

I'm struggling however to use Flex how I normally would whilst maintaining the correct semantic layout. Level 2 should be a column to the left with level 3 displayed as tiles on the right. Usually I would use a grid layout to split these two sections out but I can't seem to make that happen in this case.

For example, I can apply left:0 to the level 2 ul but I need that uls children to be on the right?

enter image description here

I've attached an image of a similar design I'd love to implement. Can anyone advise?

.container {
  background-color: lightgreen;
  min-height: 40px;
}


.nav-menu {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}

li {
  list-style: none;
}

 
li ul {
    position: absolute;
    top: 152px;
  }
<div class="container">
  <ul class="nav-menu">
    <li>
      <a href="#">Level 1</a>
      <ul>
        <li>
          <a href="#">Level 2</a>
          <ul>
            <li><a href="#">Level 3</a></li>
            <li><a href="#">Level 3</a></li>
            <li><a href="#">Level 3</a></li>
            <li><a href="#">Level 3</a></li>
            <li><a href="#">Level 3</a></li>
          </ul>
        </li>
        <li><a href="#">Level 2</a></li>
        <li><a href="#">Level 2</a></li>
        <li><a href="#">Level 2</a></li>
      </ul>
    </li>
    <li><a href="#">Level 1</a></li>
    <li><a href="#">Level 1</a></li>
    <li><a href="#">Level 1</a></li>
    <li><a href="#">Level 1</a></li>
  </ul>
</div>

Upvotes: 0

Views: 265

Answers (1)

Adam
Adam

Reputation: 5984

The trick to making this work is to ensure all your elements overlap so on hover, moving the pointer will always make sure it's inside the relevant li element. We then use position: relative on the hovered parent and position: absolute on the child to position the menu wherever we want. One issue with position: absolute is that it's hard to make them expand to full screen width as the containing block is not the screen.

For level 3, I'm keeping the containing block as the level 1 element to keep it positioned just below the nav bar.

Annotated

/* pretty = no functional value, just makes it look nice */

li, ul, body {
  margin:0; /*pretty*/
  padding:0; /*pretty*/
}

.container {
  --min-height-value: 40px;
  background-color: lightgray; /*pretty*/
  height:1px; /* added this to make .nav-menu height:100% work */
  min-height: var(--min-height-value); /*pretty*/
}

.nav-menu {
  position: relative; /* added this so we position descendants relative to the parent menu*/
  display: flex;
  flex-direction: row;
  align-content: stretch; /* also make the child elements full height so the mouse hovers within the li at all times */
  justify-content: space-between;
  height: 100%; /* this height needs to match the parent so the hover remains inside the li at all times */
  margin-inline:0.5rem;
}

.level2 {
  --top-overhang: calc(var(--min-height-value) * 0.5); /* this positions the level 2 menu below the nav-menu */
  position:absolute;
  display: flex;
  flex-direction:column;
  top:50%;
  left:0px;
  padding-top:var(--top-overhang);
  display:none;
  background-color:#eee;
  z-index:-1; /* make it appear below the nav bar, again, to keep the menu visible on hover*/
}

.nav-menu > li {
  padding-inline: 1rem; /*pretty*/
}

.nav-menu > li:hover > .level2 {
  display:block;
}

li:hover {
    background:lightgray; /*pretty*/
}

.level2 > li {
  padding-block:0.5rem; /*pretty*/
  padding-inline:2rem; /*pretty*/
}

li {
  list-style: none; /*pretty*/
}

a {
  text-decoration:none; /*pretty*/
  color:inherit; /*pretty*/
}

.nav-menu > li > a {
  display:flex;
  align-items:center;
  height:100%;
}

.level3 {
  position: absolute;
  border: 1px solid lightgray; /*pretty*/
  left:100%;
  top:var(--top-overhang);
  width:max-content;
  display:none;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: 1fr;
  gap: 0.5rem;
}

.level2 > li:hover > .level3 {
  display: grid;
}

.level3 > li {
  width: 10rem; /*pretty*/
  aspect-ratio: 1/1; /*pretty*/
  position: relative;
  display:flex;
  align-items: flex-end;
  padding: 0.5rem; /*pretty*/
}

.level3 > li::before {
  position:absolute; /*pretty*/
  content:"";
  inset:0.5rem; /*pretty*/
  bottom: 1.5rem; /*pretty*/
  border-radius: 0.5rem; /*pretty*/
  background-color:gray; /*pretty*/
}
<div class="container">
  <ul class="nav-menu">
    <li>
      <a href="#">Level 1</a>
      <ul class='level2'>
        <li>
          <a href="#">Level 2a</a>
          <ul class='level3'>
            <li><a href="#">Level 3a</a></li>
            <li><a href="#">Level 3b</a></li>
            <li><a href="#">Level 3c</a></li>
            <li><a href="#">Level 3d</a></li>
            <li><a href="#">Level 3e</a></li>
          </ul>
        </li>
        <li><a href="#">Level 2b</a></li>
        <li><a href="#">Level 2c</a>
          <ul class='level3'>
            <li><a href="#">Level 3f</a></li>
            <li><a href="#">Level 3g</a></li>
            <li><a href="#">Level 3h</a></li>
            <li><a href="#">Level 3i</a></li>
          </ul>
        </li>
        <li><a href="#">Level 2d</a></li>
      </ul>
    </li>
    <li><a href="#">Level 1</a></li>
    <li><a href="#">Level 1</a></li>
    <li><a href="#">Level 1</a></li>
    <li><a href="#">Level 1</a></li>
  </ul>
</div>

code below:

Upvotes: 0

Related Questions