<div class="center" id="center">
  <div>
    <h1>Ending loading animation</h1>
    <p>Below is a loader which has an animation loop and an end animation. When the loading is finished, it waits for the current loop to be finished before triggering.</p><p>The fake request has a minimal duration of 200ms, but it will last at least 1s (animation time).</p>
    <button type="button" id="toggleButton">Start random loading</button>
    
    <div class="overlay">
      
    <div class="loader" id="loader">
      <div class="loader__circle">
      </div>
      <div class="loader__dot loader__dot--init"></div>
      <div class="loader__dot loader__dot--finished"></div>
      <div class="loader__check">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
      </div>
    </div>
    </div>
  </div>
</div>
:root {
  --primary-color-rgb: 202, 42, 93;
  --secondary-color-rgb: 52, 208, 179;
}

body {
  -webkit-font-smoothing: antialiased;
  text-rendering: geometricPrecision;
  font-family: Montserrat;
  line-height: 1.5;
}

.center {
  display: flex;
  height: 100vh;
  width: 100vw;
  align-items: center;
  justify-content: center;
  text-align: center;

  > div {
    max-width: 768px;
    padding: 2em;
  }

  .loader {
    display: none;
  }

  &.is-loading {
    .overlay {
      opacity: 1;
      pointer-events: all;
    }
    .loader {
      display: block;
    }
  }
}

.overlay {
  opacity: 0;
  pointer-events: none;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  display: flex;
  height: 100vh;
  width: 100vw;
  align-items: center;
  justify-content: center;
  background: rgba(white, 0.88);
  transition: opacity 0.2s linear;

  .loader {
    --dot-color: rgb(var(--primary-color-rgb));
    --check-color: rgba(var(--secondary-color-rgb));

    vertical-align: middle;
  }
}
button {
  cursor: pointer;
  font: inherit;
  appearance: none;
  margin: 1em 0;
  background: rgb(var(--primary-color-rgb));
  color: white;
  padding: 0.5em 1.5em;
  border: 0;
  font-weight: 600;
  border-radius: 2em;
  box-shadow: 0 2.8px 2.2px rgba(var(--primary-color-rgb), 0.02),
    0 6.7px 5.3px rgba(var(--primary-color-rgb), 0.028),
    0 12.5px 10px rgba(var(--primary-color-rgb), 0.035),
    0 22.3px 17.9px rgba(var(--primary-color-rgb), 0.042),
    0 41.8px 33.4px rgba(var(--primary-color-rgb), 0.05),
    0 100px 80px rgba(var(--primary-color-rgb), 0.07);
}

// css loader

.loader {
  --size: 60px;
  --border: 0.04em;
  --dot-size: 0.23em;
  --dot-color: inherit;
  --check-color: green;
  --easing: cubic-bezier(0.74, 0.17, 0.29, 0.82);

  position: relative;
  display: inline-block;
  font-size: var(--size);
  width: 1em;
  height: 1em;
  transform: translateZ(0);
  color: inherit;

  &, * {
    &,
    &::before,
    &::after {
      box-sizing: border-box;
    }
  }

  &__circle {
    position: absolute;
    --calculated-position: calc(var(--dot-size) / 2 - var(--border));
    border-radius: 50%;
    top: var(--calculated-position);
    left: var(--calculated-position);
    height: calc(100% - var(--dot-size) + var(--border) * 2);
    width: calc(100% - var(--dot-size) + var(--border) * 2);
    border: var(--border) solid currentColor;
    opacity: 0.16;
  }

  &__dot {
    --background: var(--dot-color);

    position: absolute;
    top: 0;
    left: calc(50% - var(--dot-size) / 2);
    width: var(--dot-size);
    height: var(--dot-size);
    background: var(--background);
    border-radius: 50%;

    &--init {
      animation: spin 1s infinite var(--easing);
      transform-origin: 50% 0.5em;
    }

    &--finished {
      --background: var(--check-color);
      display: none;
    }
  }

  &__check {
    --check-size: calc(var(--dot-size) * 2);
    display: none;
    width: var(--check-size);
    height: var(--check-size);
    position: absolute;
    top: calc(50% - var(--check-size) / 2);
    left: calc(50% - var(--check-size) / 2);
    color: var(--check-color);

    svg {
      color: inherit;
      display: block;
      width: 100%;
      height: 100%;
    }
  }

  &.is-finishing {
    .loader__circle {
      animation: endCircle 1s both var(--easing);
    }
    .loader__dot {
      &--init {
        animation: endInit 1s both var(--easing);
      }
      &--finished {
        display: block;
        animation: end 1s both var(--easing);
      }
    }
    .loader__check {
      display: block;
      animation: check 1s both var(--easing);
    }
  }
}

@keyframes spin {
  0% {
    transform: rotate(0);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes endInit {
  0% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
    transform: translateY(calc(0.5em - var(--dot-size) / 2));
  }
}

@keyframes end {
  0% {
    opacity: 0;
  }
  50%,
  100% {
    transform: translateY(calc(0.5em - var(--dot-size) / 2));
    opacity: 1;
  }
  100% {
    transform: translateY(calc(0.5em - var(--dot-size) / 2)) scale(10);
    opacity: 0;
  }
}

@keyframes endCircle {
  0%,
  50% {
    opacity: 0.16;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(4);
  }
}

@keyframes check {
  0%,
  50% {
    transform: scale(0.25);
    opacity: 0;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}
View Compiled
const button = document.getElementById('toggleButton');
const loader = document.getElementById('loader');
const centerDiv = document.getElementById('center');
let isRequestFinished = false;

loader.addEventListener('animationiteration', () => {
  if (isRequestFinished) {
    loader.classList.add('is-finishing');
  }
})

loader.addEventListener('animationend', () => {
  setTimeout(() => {
    
  centerDiv.classList.remove('is-loading');
    loader.classList.remove('is-finishing');
    isRequestFinished = false;
  }, 1000)
})

button.addEventListener('click', () => {
  isRequestFinished = false;
  centerDiv.classList.add('is-loading')
  
  setTimeout(() => {
    isRequestFinished = true;
  }, getRandomTime(200, 10000))
})

function getRandomTime(min, max) {
  return Math.floor(Math.random() * max) + min;
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.