Search code examples
javascriptjquerycsscss-grid

Overwrite an element's style multiple times in loop?


I have a simple menu that's currently using CSS Grid that I want to be animated. However, grid-template-columns can't be smoothly animated. I am now doing the menu completely different, but in my search for a solution I came across something that I can't quite figure out. My first idea, although it might not a good idea at all and probably isn't a suitable alternative for keyframe animations, was to update styles in a while loop to animate the increasing size of the item. It doesn't work.

The relevant JS:

    $('#select-oak').hover(function () {
            let start = new Date().getTime();
            let elapsed = 0;

            while (elapsed <= 1000) {
                elapsed = new Date().getTime() - start;
                let val = elapsed / 1000;

                if (elapsed % 100 === 0) {
                    $('.selector-block').css('grid-template-columns', `${1.0 + val}fr 1fr 1fr 1fr`);
                    console.log(1 + val);
                    console.log(elapsed);
                }
            }
        }

The CSS:

.selector-block {
    position: relative;
    overflow: hidden;
    top: -32.5vh;
    background-color: white;
    gap: 5px;
    width: 100vw;
    height: 90vh;
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    clip-path: polygon(0 36%, 100% 0, 100% 60%, 0% 100%);
    z-index: 3;
}

#select-oak {
    position: relative;
    overflow: hidden;
    text-align: center;
    background-color: rgb(94, 80, 21);
    top: 21.5%;
    width: 100%;
    height: 200%;
}

#select-oak img {
    position: relative;
    left: -10vw;
    transform: scale(1.06);
    filter: blur(5px);
}

#select-oak img:hover {
    filter: none;
    transform: scale(1.1);
    transition: ease-in-out 0.5s;
}

It seemed like an unconventional approach so I wasn't surprised it didn't work, but since I am relatively new to CSS, JS, and JQuery, I'm curious as to what is stopping this from working just to help my understanding of working with JQuery and CSS. Here's the result. As you can see, it doesn't update until the last pass in the loop, even though the correct values are being printed out.

Example

So I have two questions:

  1. Is this a problem with my code, or just something you can't do?
  2. If updating styles like this is possible, is it a bad practice, and why?

Solution

  • The while loop is blocking the render process, because it is synchronous.

    I've come up with a way using CSS transitions, being agnostic of the number of child elements. It also handles the mouse leaving and even moving from one menu item to another.

    $('.selector-block > *').hover(function() {
      let frValues = Array($(this).siblings().length+1).fill('1fr');
      frValues[$(this).prevAll().length] = '2fr';
      $('.selector-block').css({
        'grid-template-columns': frValues.join(' ')
      });
    }, function() {
      let frValues = Array($(this).siblings().length+1).fill('1fr');
      $('.selector-block').css({
        'grid-template-columns': frValues.join(' ')
      });
    });
    .selector-block {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr;
      transition: all 1s ease-in-out;
    }
    
    .selector-block > * {
      border: solid red 1px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div class="selector-block"><div id="select-oak">select-oak</div><div>foobar</div><div>baz</div><div>bat</div></div>

    You could do this with CSS only, no JavaScript, if the number of child elements is fixed, via the :nth-child() selector and :hover() as well, but this way, you can dynamically add elements to your menu any time. Actually, you couldn't, as you would need to trigger the parent's property animation from the hover of a child, which is not possible.

    Old answer:

    You're looking for window.requestAnimationFrame(). Here's a basic demonstration of how it would work. You'll have to tweak your values for a smoother transition and handle the mouse leaving the element of course.

    let start;
    let elapsed = 0;
    let rafHandle;
    
    function hoverAnimation() {
      if (elapsed <= 1000) {
        elapsed = new Date().getTime() - start;
        let val = elapsed / 1000;
    
        $('.selector-block').css('grid-template-columns', `${1.0 + val}fr 1fr 1fr 1fr`);
        //console.log(1 + val);
        //console.log(elapsed);
        window.setTimeout(function() {
          rafHandle = window.requestAnimationFrame(hoverAnimation);
        }, 1000/60); // about 60 FPS
      }
    }
    
    $('#select-oak').hover(function() {
      start = new Date().getTime();
      elapsed = 0;
      rafHandle = window.requestAnimationFrame(hoverAnimation);
    }, function() {
      // handle mouse leave animation here instead
      window.cancelAnimationFrame(rafHandle);
      elapsed = Infinity;
      $('.selector-block').css('grid-template-columns', `1fr 1fr 1fr 1fr`);
    });
    .selector-block {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div class="selector-block"><div id="select-oak">select-oak</div><div>foobar</div><div>baz</div><div>bat</div></div>