skip to Main Content

I’m building a small sidebar element to a web page which has multiple hidden .sidebar-contents. What it does, is reveal .sidebar-contents within a few seconds of delay between each items whenever I click on the button. Then hides the content in reverse order right after clicking the button again.

It worked just fine in my first try, but when I try it multiple times, it looks like class is being added twice in the sidebar. I bet I’m doing something wrong here but I don’t have a clue.

Any help would be appreciated.

const SIDEBAR_DELAY_TIME = 150;

function handleSidebar() {
  const btn = document.querySelector('button');
  const sidebar = document.querySelector('.sidebar');
  const contents = document.querySelectorAll('.sidebar-element');

  if (!btn || !sidebar) return;

  btn.addEventListener('click', () => {
    if (!sidebar.classList.contains('active')) {
      sidebar.classList.add('active');
      contents.forEach((item, i) => {
        setTimeout(() => {
          item.classList.add('active');
        }, i * SIDEBAR_DELAY_TIME);
      });
    } else {
      const keys = Array.from({
        length: contents.length
      }, (_, i) => i);
      keys.reverse();

      keys.forEach((key, i) => {
        setTimeout(() => {
          contents[key].classList.remove('active');
        }, i * SIDEBAR_DELAY_TIME);

        if (key === 0) {
          contents[0].addEventListener('transitionend', () => {
            sidebar.classList.remove('active');
          });
        }
      });
    }
  });
}

handleSidebar();
:root {
  --transition: 0.3s ease;
}

.sidebar {
  width: 450px;
  transform: translateY(55px);
  transition: transform var(--transition), opacity var(--transition);
  opacity: 0;
}

.sidebar.active {
  transform: translateY(0);
  opacity: 1;
}

.sidebar-element {
  padding: 16px;
  border: 1px solid #333;
  transform: translateY(64px);
  transition: transform var(--transition), opacity var(--transition);
  opacity: 0;
}

.sidebar-element.active {
  transform: translateY(0);
  opacity: 1;
}
<button>click me</button>

<aside class="sidebar">
  <div class="sidebar-element">
    Element A
  </div>
  <div class="sidebar-element">
    Element B
  </div>
  <div class="sidebar-element">
    Element C
  </div>
  <div class="sidebar-element">
    Element D
  </div>
</aside>

2

Answers


  1. The problem is here:

              contents[0].addEventListener('transitionend', () => {
                sidebar.classList.remove('active');
              });
    

    You are adding the transitionend listener multiple times. And the event is triggered after animations in both directions end. This ensues chaos.

    You need to remove the listener after the transition ends

      const transitionend = () => {
        sidebar.classList.remove('active');
        contents[0].removeEventListener('transitionend', transitionend)
      };
    // ...
    contents[0].addEventListener('transitionend', transitionend)
    
    const SIDEBAR_DELAY_TIME = 150;
    
    function handleSidebar() {
      const btn = document.querySelector('button');
      const sidebar = document.querySelector('.sidebar');
      const contents = document.querySelectorAll('.sidebar-element');
    
      const transitionend = () => {
        sidebar.classList.remove('active');
        contents[0].removeEventListener('transitionend', transitionend)
      };
      if (!btn || !sidebar) return;
    
      btn.addEventListener('click', () => {
        if (!sidebar.classList.contains('active')) {
          sidebar.classList.add('active');
          contents.forEach((item, i) => {
            setTimeout(() => {
              item.classList.add('active');
            }, i * SIDEBAR_DELAY_TIME);
          });
        } else {
          const keys = Array.from({
            length: contents.length
          }, (_, i) => i);
          keys.reverse();
    
          keys.forEach((key, i) => {
            setTimeout(() => {
              contents[key].classList.remove('active');
            }, i * SIDEBAR_DELAY_TIME);
    
            if (key === 0) {
              contents[0].addEventListener('transitionend', transitionend)
            }
          });
        }
      });
    }
    
    handleSidebar();
    :root {
      --transition: 0.3s ease;
    }
    
    .sidebar {
      width: 450px;
      transform: translateY(55px);
      transition: transform var(--transition), opacity var(--transition);
      opacity: 0;
    }
    
    .sidebar.active {
      transform: translateY(0);
      opacity: 1;
    }
    
    .sidebar-element {
      padding: 16px;
      border: 1px solid #333;
      transform: translateY(64px);
      transition: transform var(--transition), opacity var(--transition);
      opacity: 0;
    }
    
    .sidebar-element.active {
      transform: translateY(0);
      opacity: 1;
    }
    <button>click me</button>
    
    <aside class="sidebar">
      <div class="sidebar-element">
        Element A
      </div>
      <div class="sidebar-element">
        Element B
      </div>
      <div class="sidebar-element">
        Element C
      </div>
      <div class="sidebar-element">
        Element D
      </div>
    </aside>
    Login or Signup to reply.
  2. As mentioned in the comments, the transition-end is attached each time the ‘set inactive’ block is executed. That means that not only it is attached multiple times, but also that it will run for the ‘set active’ part after that.

    One solution would be to add it once, not in the click event. Also below is a quickly gobbled up suggestion to prevent repetition of code (might need some tweaking)
    Amongst others, by introducing a helper function to set the class of an element to whatever the status is (with ‘toggle’ )

    const SIDEBAR_DELAY_TIME = 150;
    
    function handleSidebar() {
      const btn = document.querySelector('button');
      const sidebar = document.querySelector('.sidebar');
      const contents =  document.querySelectorAll('.sidebar-element'), count = contents.length;
    
      if (!btn || !sidebar) return;
    
        let active = false;
      const set = el => el.classList.toggle('active',active);
      contents[contents.length-1].addEventListener('transitionend', () => set(sidebar));
              
      btn.addEventListener('click', () => {
        active=!active;
        if (active) set(sidebar);      
          contents.forEach((item, i) => {
            setTimeout(() => set(item), (active ? i : count - i) * SIDEBAR_DELAY_TIME);
     
      });
    });
    }
    
    handleSidebar();
    :root {
      --transition: 0.3s ease;
    }
    
    .sidebar {
      width: 450px;
      transform: translateY(55px);
      transition: transform var(--transition), opacity var(--transition);
      opacity: 0;
    }
    
    .sidebar.active {
      transform: translateY(0);
      opacity: 1;
    }
    
    .sidebar-element {
      padding: 16px;
      border: 1px solid #333;
      transform: translateY(64px);
      transition: transform var(--transition), opacity var(--transition);
      opacity: 0;
    }
    
    .sidebar-element.active {
      transform: translateY(0);
      opacity: 1;
    }
    <button>click me</button>
    
    <aside class="sidebar">
      <div class="sidebar-element">
        Element A
      </div>
      <div class="sidebar-element">
        Element B
      </div>
      <div class="sidebar-element">
        Element C
      </div>
      <div class="sidebar-element">
        Element D
      </div>
    </aside>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search