<div class="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>

<br>

<button onclick="toggleClass()">Toggle finishing class</button>
@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;
  }
}

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

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

  position: relative;
  display: inline-block;

  /* for relative size inside the loader */
  font-size: var(--size);
  width: 1em;
  height: 1em;

  /* for smoother animations */
  transform: translateZ(0);

  color: inherit;
}

.loader__circle {
  --calculated-position: calc(var(--dot-size) / 2 - var(--border));

  position: absolute;
  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;
}

.loader__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%;

  /* let's animate it */
  animation: spin 1s infinite var(--easing);
  transform-origin: 50% 0.5em;
}

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

.loader__dot--finished {
  --background: var(--check-color);

  display: none;
}

.loader__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);
}

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

.loader.is-finishing .loader__circle {
  animation: endCircle 1s both var(--easing);
}

.loader.is-finishing .loader__dot--init {
  animation: endInit 1s both var(--easing);
}

.loader.is-finishing .loader__dot--finished {
  display: block;
  animation: end 1s both var(--easing);
}

.loader.is-finishing .loader__check {
  display: block;
  animation: check 1s both var(--easing);
}

button {
  position: relative;
  z-index: 2;
}
function toggleClass() {
  document.querySelector('.loader').classList.toggle('is-finishing')
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.