#app
  .main-desc
    | see more details in 
    a(href="https://vuejs.org/guide/built-ins/transition.html", target="_blank") Vue transition
  transition(name="fade", @enter="onWrapperEnter")
    .modals-wrapper(v-if="initWrapper")
      transition-group(name="modals", @after-leave="onModalLeave")
        template(v-for="(data, key) in modalList", :key="key")
          .modal-mask(v-if="initModal && data")
            .modal-wrapper
              .title(:style="{color: data.color}") {{ data.name }}
              .close-btn(@click="removeModal") close
  .open-btn(@click="addModal") open
View Compiled
@import url('https://fonts.googleapis.com/css2?family=Rubik+Puddles&display=swap&text=abcdefghijklmnopqrstuvwxyz0123456789');

@import url('https://fonts.googleapis.com/css2?family=Concert+One&display=swap');

* {
  box-sizing: border-box;
}
html {
  font-size: 100vmax / 1600 * 100;
  @media (max-width: 992px) {
    font-size: 60px;
  }
}

@mixin flexCenter {
  display: flex;
  justify-content: center;
  align-items: center;
}
@mixin fullCover($pos: absolute) {
  position: $pos;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
$default-font: 'Concert One', cursive;
body {
  position: relative;
  min-height: 100vh;
  @include flexCenter;
  font-family: $default-font;
  font-size: 0.24rem;
  text-align: center;
  background: url(https://images.unsplash.com/photo-1539096522021-2de4703b0271?crop=entropy&cs=srgb&fm=jpg&ixid=MnwxNDU4OXwwfDF8cmFuZG9tfHx8fHx8fHx8MTY0OTIyNDQ4Nw&ixlib=rb-1.2.1&q=85) no-repeat center 30% / cover;
}
.main-desc {
  border-radius: 0.2rem;
  background-color: #fff;
  padding: 0.1rem 0.3rem;
}
.open-btn {
  position: absolute;
  bottom: 0.2rem;
  right: 0.2rem;
  padding: 0.1rem 0.3rem;
  color: #ffcc33;
  text-transform: uppercase;
  border-radius: 0.2rem;
  background-color: #333;
  box-shadow: 3px 5px 0 #ccc;
  cursor: pointer;
  z-index: 100;
}
.modals-wrapper {
  @include fullCover(fixed);
  background-color: rgba(#000, 0.5);
}
.modal-mask {
  @include fullCover;
  @include flexCenter;
  .modal-wrapper {
    position: relative;
    width: 5rem;
    height: 3rem;
    @include flexCenter;
    background-color: #333;
    border-radius: 0.2rem;
    filter: drop-shadow(0 0 10px rgba(#000, 0.5));
    &:after {
      content: '';
      @include fullCover;
      backdrop-filter: brightness(0.3);
      z-index: 1;
    }
  }
  &:last-of-type .modal-wrapper {
    &:after {
      display: none;
    }
  }
  .title {
    font-size: 0.6rem;
    font-family: 'Rubik Puddles', $default-font;
  }
  .close-btn {
    position: absolute;
    top: 0.1rem;
    right: 0.2rem;
    color: #ffcc33;
    font-size: 0.2rem;
    text-transform: uppercase;
    cursor: pointer;
  }
}

// animation
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.modals-enter-active,
.modals-leave-active {
  transition: opacity 0.3s;
  .modal-wrapper,
  .rule-modal-wrapper {
    transition: transform 0.3s;
  }
}
.modals-enter-from,
.modals-leave-to {
  opacity: 0;
  .modal-wrapper,
  .rule-modal-wrapper {
    transform: translateY(20%);
  }
}
View Compiled
// utils
const randomHash = () => Math.random().toString(36).substring(2);
const randomRange = (min, max) => Math.floor(Math.random() * (max - min + 1) ) + min;
const getRandomColor = () => {
  const colorDeg = randomRange(0, 360);
  const contrast = randomRange(30, 50);
  const lightness = randomRange(60, 80);
  return `hsl(${colorDeg}deg, ${contrast}%, ${lightness}%)`;
}


const { ref, watch, onMounted }  = Vue;
const App = {
  setup() {
    const initWrapper = ref(false);
    const initModal = ref(false);
    const modalList = ref([]);
    
    watch(() => modalList.value.length, (val) => {
      if (val && !initWrapper.value) initWrapper.value = true;
    })
    
    // transition
    const onWrapperEnter = () => initModal.value = true;
    const onModalLeave = () => {
      if (modalList.value.length) return;
      initWrapper.value = false;
      initModal.value = false;
    }
    // modal
    const addModal = () => {
      modalList.value.push({
        name: randomHash(),
        color: getRandomColor(),
      });
    }
    const removeModal = () => {
      modalList.value.pop();
    }
    
    onMounted(() => addModal());
    
    return {
      initWrapper,
      initModal,
      modalList,
      onWrapperEnter,
      onModalLeave,
      addModal,
      removeModal,
    }
  },
}

Vue.createApp(App).mount('#app');

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/vue@3.2.31/dist/vue.global.js