Search code examples
htmlcssanimationcss-animations

Layer of one element stacking over another during animation


I am trying to achieve the following:

  1. Filling a card from bottom to top with the specified color (this works) on hover.
  2. Still on hovering the card, I want to display a rocket launch animation.

I have the problem that the .rocket::before element does not want to be displayed behind the .rocket element.

I have the following code:

.main-nav-card {
  background: linear-gradient(
    to bottom,
    transparent 50%,
    #1c7ed6 50%
  );
  background-size: 100% 200%;
  height: 70rem;
  border: 1px solid currentColor;
  padding: 4.8rem 9.6rem;
  border-radius: var(--border-radius-large);
  position: relative;
  z-index: 2;
}

.main-nav-card:hover {
  animation-name: fillCard;
  animation-duration: 0.5s;
  animation-fill-mode: forwards;
}

@keyframes fillCard {
  from {
    background-position: top;
  }

  to {
    background-position: bottom;
  }
}

.rocket {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  background-color: #000;
  height: 50px;
  width: 25px;
  border-radius: 50% 50% 0 0;
  /* z-index: 20; */
}

.rocket::before {
  content: "";
  position: absolute;
  bottom: 0;
  right: -15px;

  height: 20px;
  width: 55px;
  border-radius: 50% 50% 0 0;
  background-color: #f03e3e;

  z-index: -1;
}

.rocket::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
  bottom: -4px;

  height: 4px;
  width: 15px;
  border-radius: 0 0 2px 2px;
  background-color: #f03e3e;
  z-index: -1;
}

.main-nav-card:hover .rocket {
  display: block;
  animation-name: flyRocket;
  animation-duration: 3s;
}

@keyframes flyRocket {
  from {
    transform: translateY(1000%);
    /* z-index: 9999; */
  }

  to {
    transform: translateY(0);
    /* z-index: 9999; */
  }
}

.card-title {
  font-size: 3.2rem;
}

.window {
  height: 10px;
  width: 10px;
  background-color: var(--color-primary);
  border-radius: 50%;
  position: relative;
  top: 17px;
  left: 7.5px;
  /* transform: translateX(25%); */
}
<div class="main-nav-card">
  <p class="card-title">Introduction</p>

  <div class="rocket">
    <div class="window"></div>
  </div>
</div>

What am I doing wrong?


Solution

  • I believe this issue is due to the CSS "transform" property creating a new stacking context on the rocket, which prevents child elements and pseudo elements from appearing behind the stacking context root (.rocket). As a result, the fix is to put your .rocket element in some wrapper element and apply the transform on the wrapper instead. That way, the stacking context is created on the wrapper rather than on the rocket directly, enabling any child and pseudo elements to appear behind it once again.

    However, there is a better way to do this, using the translateZ() and preserve-3d. Specifically, instead of managing z-indexes on your pseudo elements, use transform: translateZ(-1px); to put them behind the parent element. Then, all you have to do to make it work is apply transform-style: preserve-3d; to the .rocket selector. This works in all most modern browsers. Here is the full code:

    .main-nav-card {
      background: linear-gradient(to bottom, transparent 50%, #1c7ed6 50%);
      background-size: 100% 200%;
      height: 70rem;
      border: 1px solid currentColor;
      padding: 4.8rem 9.6rem;
      border-radius: var(--border-radius-large);
      position: relative;
      z-index: 2;
    }
    
    .main-nav-card:hover {
      animation-name: fillCard;
      animation-duration: 0.5s;
      animation-fill-mode: forwards;
    }
    
    @keyframes fillCard {
      from {
        background-position: top;
      }
      to {
        background-position: bottom;
      }
    }
    
    .rocket {
      display: none;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      background-color: #000;
      height: 50px;
      width: 25px;
      border-radius: 50% 50% 0 0;
      transform-style: preserve-3d;
    }
    
    .rocket::before {
      content: "";
      position: absolute;
      bottom: 0;
      right: -15px;
      height: 20px;
      width: 55px;
      border-radius: 50% 50% 0 0;
      background-color: #f03e3e;
      transform: translateZ(-1px);
    }
    
    .rocket::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      margin: auto;
      bottom: -4px;
      height: 4px;
      width: 15px;
      border-radius: 0 0 2px 2px;
      background-color: #f03e3e;
      transform: translateZ(-1px);
    }
    
    .main-nav-card:hover .rocket {
      display: block;
      animation-name: flyRocket;
      animation-duration: 3s;
    }
    
    @keyframes flyRocket {
      from {
        transform: translateY(1000%);
      }
      to {
        transform: translateY(0);
      }
    }
    
    .card-title {
      font-size: 3.2rem;
    }
    
    .window {
      height: 10px;
      width: 10px;
      background-color: var(--color-primary);
      border-radius: 50%;
      position: relative;
      top: 17px;
      left: 7.5px;
      /* transform: translateX(25%); */
    }
    <div class="main-nav-card">
      <p class="card-title">Introduction</p>
    
      <div class="rocket">
        <div class="window"></div>
      </div>
    </div>

    Edit: Apparently this doesn't work for iOS Safari because perserve-3d is somewhat broken there. So if you really want to support all browsers, you should use the wrapper element technique.