javascripthtmlcssflexboxabsolute

How to make the li element in my project not move?


I have a sidebar menu which has an indicator that shows the button which is selected.

The result I was expecting was that the indicator moved but the li elements wouldn't. But turns out they both move. I know the way I coded it is very unnecessary but I don't know how to do it otherwise. Can someone explain to me why the li elements move, how can I make my code more efficient and how to fix my problem?

const menuButtonElements   = document.querySelectorAll('#menu-sdbr-list-item button');
const menuIndicatorElement = document.getElementById('menu-sdbr-list-indicator');
const menuSidebarUlElement = document.getElementById('menu-sdbr-list');
let selectedButtonIndex = 0;

menuButtonElements.forEach((value, index) => {
  value.addEventListener('click', () => {
    selectedButtonIndex = index;
    menuIndicatorElement.setAttribute('style', `
    margin-bottom: ${(index + 1) * -65}px;
    `)
    menuButtonElements.forEach(value => {
      value.setAttribute('style', `
      color: var(--color-surface-300);
      `)
    })
    menuButtonElements[index].setAttribute('style', `
    color: white;
    `)
  })
})
@import url('https://fonts.googleapis.com/css2?family=Fira+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@import "https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round";
body {
  --color-primary-100: rgb(105, 54, 245);
  --color-primary-200: rgb(128, 78, 247);
  --color-primary-300: rgb(148, 100, 249);
  --color-primary-400: rgb(166, 122, 251);
  --color-primary-500: rgb(183, 144, 252);
  --color-primary-600: rgb(199, 165, 253);
  --color-surface-100: rgb(0, 0, 0);
  --color-surface-200: rgb(30, 30, 30);
  --color-surface-300: rgb(53, 53, 53);
  --color-surface-400: rgb(78, 78, 78);
  --color-surface-500: rgb(105, 105, 105);
  --color-surface-600: rgb(133, 133, 133);
  --color-surface-mixed-100: rgb(26, 22, 37);
  --color-surface-mixed-200: rgb(40, 35, 48);
  --color-surface-mixed-300: rgb(63, 58, 70);
  --color-surface-mixed-400: rgb(88, 83, 94);
  --color-surface-mixed-500: rgb(113, 109, 119);
  --color-surface-mixed-600: rgb(140, 136, 144);
  --color-primary-100-mix: 105, 54, 245;
  --color-primary-500-mix: 183, 144, 252;
  font-family: Fira Sans Condensed, Arial;
  margin: 0;
}

.menu-sidebar-overlay {
  position: fixed;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 200;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: white;
}

.menu-sidebar {
  position: relative;
  height: 100%;
  width: 35vw;
  background-color: var(--color-primary-100);
  display: flex;
}

.menu-sidebar .rectangle {
  width: 15%;
  height: 100%;
  background-color: var(--color-surface-300);
}

.menu-sdbr-title {
  display: inline;
  color: var(--color-primary-200);
  font-size: 40px;
  margin: 15px 0 0 10px;
  color: white;
}

.menu-sdbr-contents {
  width: 100%;
  position: relative;
  height: 100%;
  flex: 1;
  display: flex;
  flex-direction: column;
  margin-left: 10px;
}

.menu-sdbr-list {
  position: absolute;
  width: 100%;
  padding: 0;
  margin: 90px 0 0 0;
  list-style-type: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  flex: 1;
  height: 100%;
}

.menu-sdbr-list-indicator {
  margin-bottom: -65px;
  width: 100%;
  height: 60px;
  background-color: var(--color-surface-300);
  z-index: 250;
  border-top-left-radius: 10px;
  border-bottom-left-radius: 10px;
  transition: margin-bottom 150ms;
}

.menu-sdbr-list-item button {
  position: relative;
  background-color: transparent;
  border: 0 solid rgba(0, 0, 0, 1);
  width: 100%;
  padding: 0 15px;
  height: 60px;
  text-align: left;
  display: flex;
  align-items: center;
  color: var(--color-surface-300);
  z-index: 300;
  border-top-left-radius: 10px;
  border-bottom-left-radius: 10px;
  font-size: 15px;
  font-weight: 400;
  z-index: 300;
  /*
border: 1px solid rgba(0, 0, 0, 1);
border-right: none;
*/
  transition: color 150ms;
}

.menu-sdbr-list-item button .material-icons {
  margin-right: 15px;
  font-size: 20px;
}
<nav class="menu-sidebar-overlay">
  <div class="menu-sidebar">
    <div class="menu-sdbr-contents">
      <h1 class="menu-sdbr-title">MENU</h1>
      <ul class="menu-sdbr-list" id="menu-sdbr-list">
        <li class="menu-sdbr-list-indicator" id="menu-sdbr-list-indicator">
        </li>
        <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
          <button>
                  <span class="material-icons">dark_mode</span>
                  Dark Mode
                </button>
        </li>
        <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
          <button>
                  <span class="material-icons">settings</span>
                  Settings
                </button>
        </li>
        <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
          <button>
                  <span class="material-icons">logout</span>
                  Log Out
                </button>
        </li>
      </ul>
    </div>
    <div class="rectangle"></div>
  </div>
</nav>


Solution

  • Rather than move an absolutely positioned element up and down, it would be easier to add and remove a class to the parent li and then add the background to that and highlight to the button based on the class.

    This also has the added benefit of allowing you to add that class to one of the li if you want it pre-selected, in the example below, I have added it to the first li.

    const menuButtonElements = document.querySelectorAll('#menu-sdbr-list-item button');
    
    // change the name of this foreach variable so you don't have matching var name inside the closure
    menuButtonElements.forEach(button => {
      button.addEventListener('click', () => {
        // remove class from all button parent lis
        menuButtonElements.forEach(value => {
          value.parentNode.classList.remove('selected');
        });
        
        // add class to the clicked button parent li
        button.parentNode.classList.add('selected');
      })
    })
    @import url('https://fonts.googleapis.com/css2?family=Fira+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
    @import "https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round";
    body {
      --color-primary-100: rgb(105, 54, 245);
      --color-primary-200: rgb(128, 78, 247);
      --color-primary-300: rgb(148, 100, 249);
      --color-primary-400: rgb(166, 122, 251);
      --color-primary-500: rgb(183, 144, 252);
      --color-primary-600: rgb(199, 165, 253);
      --color-surface-100: rgb(0, 0, 0);
      --color-surface-200: rgb(30, 30, 30);
      --color-surface-300: rgb(53, 53, 53);
      --color-surface-400: rgb(78, 78, 78);
      --color-surface-500: rgb(105, 105, 105);
      --color-surface-600: rgb(133, 133, 133);
      --color-surface-mixed-100: rgb(26, 22, 37);
      --color-surface-mixed-200: rgb(40, 35, 48);
      --color-surface-mixed-300: rgb(63, 58, 70);
      --color-surface-mixed-400: rgb(88, 83, 94);
      --color-surface-mixed-500: rgb(113, 109, 119);
      --color-surface-mixed-600: rgb(140, 136, 144);
      --color-primary-100-mix: 105, 54, 245;
      --color-primary-500-mix: 183, 144, 252;
      font-family: Fira Sans Condensed, Arial;
      margin: 0;
    }
    
    .menu-sidebar-overlay {
      position: fixed;
      background-color: rgba(0, 0, 0, 0.5);
      z-index: 200;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: flex-end;
      color: white;
    }
    
    .menu-sidebar {
      position: relative;
      height: 100%;
      width: 35vw;
      background-color: var(--color-primary-100);
      display: flex;
    }
    
    .menu-sidebar .rectangle {
      width: 15%;
      height: 100%;
      background-color: var(--color-surface-300);
    }
    
    .menu-sdbr-title {
      display: inline;
      color: var(--color-primary-200);
      font-size: 40px;
      margin: 15px 0 0 10px;
      color: white;
    }
    
    .menu-sdbr-contents {
      width: 100%;
      position: relative;
      height: 100%;
      flex: 1;
      display: flex;
      flex-direction: column;
      margin-left: 10px;
    }
    
    .menu-sdbr-list {
      position: absolute;
      width: 100%;
      padding: 0;
      margin: 90px 0 0 0;
      list-style-type: none;
      display: flex;
      flex-direction: column;
      justify-content: center;
      gap: 5px;
      flex: 1;
      height: 100%;
    }
    
    .menu-sdbr-list-item button {
      position: relative;
      background-color: transparent;
      border: 0 solid rgba(0, 0, 0, 1);
      width: 100%;
      padding: 0 15px;
      height: 60px;
      text-align: left;
      display: flex;
      align-items: center;
      color: var(--color-surface-300);
      z-index: 300;
      border-top-left-radius: 10px;
      border-bottom-left-radius: 10px;
      font-size: 15px;
      font-weight: 400;
      z-index: 300;
      /*
    border: 1px solid rgba(0, 0, 0, 1);
    border-right: none;
    */
      transition: color 150ms;
    }
    
    .menu-sdbr-list-item button .material-icons {
      margin-right: 15px;
      font-size: 20px;
    }
    
    /* the below are the new classes for the selected item */
    .menu-sdbr-list-item.selected {
      background-color: var(--color-surface-300);
      border-top-left-radius: 10px;
      border-bottom-left-radius: 10px;
    }
    
    .menu-sdbr-list-item.selected button {
        color: white;
    }
    <nav class="menu-sidebar-overlay">
      <div class="menu-sidebar">
        <div class="menu-sdbr-contents">
          <h1 class="menu-sdbr-title">MENU</h1>
          <ul class="menu-sdbr-list" id="menu-sdbr-list">
            <li class="menu-sdbr-list-item selected" id="menu-sdbr-list-item">
              <button>
                    <span class="material-icons">dark_mode</span>
                    Dark Mode
                  </button>
            </li>
            <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
              <button>
                    <span class="material-icons">settings</span>
                    Settings
                  </button>
            </li>
            <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
              <button>
                    <span class="material-icons">logout</span>
                    Log Out
                  </button>
            </li>
          </ul>
        </div>
        <div class="rectangle"></div>
      </div>
    </nav>

    Sorry, just realised you had an animation on your background for it to slide between the clicked elements - the above answer wouldn't do that, instead you would need to use absolute positioning to move the highlight without the rest of it moving:

    const menuButtonElements   = document.querySelectorAll('#menu-sdbr-list-item button');
    const menuIndicatorElement = document.getElementById('menu-sdbr-list-indicator');
    const menuSidebarUlElement = document.getElementById('menu-sdbr-list');
    let selectedButtonIndex = 0;
    
    const elementHeight = 65;
    const initialPosition = -((menuButtonElements.length - 1) / 2) * elementHeight;
    
    menuButtonElements.forEach((value, index) => {
      value.addEventListener('click', () => {
        selectedButtonIndex = index;
        menuIndicatorElement.setAttribute('style', `
        margin-top: ${initialPosition + (index * elementHeight)}px;
        `)
        menuButtonElements.forEach(value => {
          value.setAttribute('style', `
          color: var(--color-surface-300);
          `)
        })
        menuButtonElements[index].setAttribute('style', `
        color: white;
        `)
      })
    })
    @import url('https://fonts.googleapis.com/css2?family=Fira+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
    @import "https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round";
    body {
      --color-primary-100: rgb(105, 54, 245);
      --color-primary-200: rgb(128, 78, 247);
      --color-primary-300: rgb(148, 100, 249);
      --color-primary-400: rgb(166, 122, 251);
      --color-primary-500: rgb(183, 144, 252);
      --color-primary-600: rgb(199, 165, 253);
      --color-surface-100: rgb(0, 0, 0);
      --color-surface-200: rgb(30, 30, 30);
      --color-surface-300: rgb(53, 53, 53);
      --color-surface-400: rgb(78, 78, 78);
      --color-surface-500: rgb(105, 105, 105);
      --color-surface-600: rgb(133, 133, 133);
      --color-surface-mixed-100: rgb(26, 22, 37);
      --color-surface-mixed-200: rgb(40, 35, 48);
      --color-surface-mixed-300: rgb(63, 58, 70);
      --color-surface-mixed-400: rgb(88, 83, 94);
      --color-surface-mixed-500: rgb(113, 109, 119);
      --color-surface-mixed-600: rgb(140, 136, 144);
      --color-primary-100-mix: 105, 54, 245;
      --color-primary-500-mix: 183, 144, 252;
      font-family: Fira Sans Condensed, Arial;
      margin: 0;
    }
    
    .menu-sidebar-overlay {
      position: fixed;
      background-color: rgba(0, 0, 0, 0.5);
      z-index: 200;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: flex-end;
      color: white;
    }
    
    .menu-sidebar {
      position: relative;
      height: 100%;
      width: 35vw;
      background-color: var(--color-primary-100);
      display: flex;
    }
    
    .menu-sidebar .rectangle {
      width: 15%;
      height: 100%;
      background-color: var(--color-surface-300);
    }
    
    .menu-sdbr-title {
      display: inline;
      color: var(--color-primary-200);
      font-size: 40px;
      margin: 15px 0 0 10px;
      color: white;
    }
    
    .menu-sdbr-contents {
      width: 100%;
      position: relative;
      height: 100%;
      flex: 1;
      display: flex;
      flex-direction: column;
      margin-left: 10px;
    }
    
    .menu-sdbr-list {
      position: absolute;
      width: 100%;
      padding: 0;
      margin: 90px 0 0 0;
      list-style-type: none;
      display: flex;
      flex-direction: column;
      justify-content: center;
      gap: 5px;
      flex: 1;
      height: 100%;
    }
    
    .menu-sdbr-list-indicator {
      position: absolute;
      top: 50%;
      margin-top: -65px;
      right: 0;
      width: 100%;
      height: 60px;
      background-color: var(--color-surface-300);
      z-index: 250;
      transform: translateY(-50%);
      border-top-left-radius: 10px;
      border-bottom-left-radius: 10px;
      transition: margin-top 150ms;
    }
    
    .menu-sdbr-list-item button {
      position: relative;
      background-color: transparent;
      border: 0 solid rgba(0, 0, 0, 1);
      width: 100%;
      padding: 0 15px;
      height: 60px;
      text-align: left;
      display: flex;
      align-items: center;
      color: var(--color-surface-300);
      z-index: 300;
      border-top-left-radius: 10px;
      border-bottom-left-radius: 10px;
      font-size: 15px;
      font-weight: 400;
      z-index: 300;
      /*
    border: 1px solid rgba(0, 0, 0, 1);
    border-right: none;
    */
      transition: color 150ms;
    }
    
    .menu-sdbr-list-item button .material-icons {
      margin-right: 15px;
      font-size: 20px;
    }
    <nav class="menu-sidebar-overlay">
      <div class="menu-sidebar">
        <div class="menu-sdbr-contents">
          <h1 class="menu-sdbr-title">MENU</h1>
          <ul class="menu-sdbr-list" id="menu-sdbr-list">
            <li class="menu-sdbr-list-indicator" id="menu-sdbr-list-indicator">
            </li>
            <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
              <button style="color: white">
                      <span class="material-icons">dark_mode</span>
                      Dark Mode
                    </button>
            </li>
            <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
              <button>
                      <span class="material-icons">settings</span>
                      Settings
                    </button>
            </li>
            <li class="menu-sdbr-list-item" id="menu-sdbr-list-item">
              <button>
                      <span class="material-icons">logout</span>
                      Log Out
                    </button>
            </li>
          </ul>
        </div>
        <div class="rectangle"></div>
      </div>
    </nav>

    The initial margin-top is based on the number of elements above the middle element there is - as there are three elements and each element is 60px height with 5px gap, there is one element above therefore you margin -65px. If there were 4 elements there would be 1.5 elements above so 1.5 * -65.

    As you can see in the js, this is worked out using number of elements minus one then divided by 2 (gets the middle position), then the result is multiplied by the height