<div class="container">
  <div class="loading">
    <div class="loading-content">
      <svg viewBox="0 0 128 128" width="128" height="128" xmlns="http://www.w3.org/2000/svg" class="loading-icon loading-icon--back">
        <path d="M64 1 A63 63 0 1 1 64 127 A63 63 0 1 1 64 1" stroke="#ccc" stroke-width="2" fill="none"/>
      </svg>
      <svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" class="loading-icon loading-icon--front">
        <path d="M64 1 A63 63 0 1 1 64 127 A63 63 0 1 1 64 1" stroke="#333" stroke-width="2" fill="none"/>
      </svg>
    </div>
    <span class="loading-text">0%</span>
  </div>
  <p class="notice">Click anywhere!</p>
</div>
.container {
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  color: #555;
  font-family: 'Roboto Mono', monospace;
}
.loading {
  width: 128px;
  height: 128px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.loading-content {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}
.loading-icon {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  stroke-dasharray: 9999;
  stroke-dashoffset: 9999;
}
.notice {
  margin-top: 20px;
}
View Compiled
class Loading {
  constructor() {
    this.el = {
      loading: document.querySelector('.loading'),
      content: document.querySelector('.loading-content'),
      text: document.querySelector('.loading-text'),
      front: {
        svg: document.querySelector('.loading-icon--front'),
        path: document.querySelector('.loading-icon--front path'),
      },
      back: {
        svg: document.querySelector('.loading-icon--back'),
        path: document.querySelector('.loading-icon--back path'),
      },
    }
    this.pathLength = 0
    this.isHiding = false
    this.tween = {
      progress: 0
    }
  }
  
  show() {
    this.pathLength = this.el.front.path.getTotalLength()
    this.el.front.path.style.strokeDasharray = this.pathLength
    this.el.front.path.style.strokeDashoffset = this.pathLength * 3
    this.el.back.path.style.strokeDasharray = this.pathLength
    this.el.back.path.style.strokeDashoffset = this.pathLength

    gsap.fromTo(this.el.back.path, {
      strokeDashoffset: this.pathLength,
    }, {
      strokeDashoffset: 0,
      duration: 1.2,
      ease: 'expo.out'
    })
  }
  
  /*
   * 0 to 1
   */
  async progress(progress) {
    if (progress < 1) {
      this.updateText(progress, {
        duration: 0.6,
        ease: 'power1.out' 
      })

      await gsap.to(this.el.front.path, {
        strokeDashoffset: this.pathLength * (3 - progress),
        duration: 0.8,
        ease: 'power3.out'
      }).then()
    } else {
      this.updateText(progress, {
        duration: 0.8,
        ease: 'power1.inOut' 
      })

      await this.hide()
    }
  }
  
  async updateText(progress, { duration = 0.6, ease = 'power1.out' } = {}) {
    await gsap.to(this.tween, {
      progress,
      duration,
      ease,
      onUpdate: () => {
        const percent = Math.floor(this.tween.progress * 100)

        this.el.text.textContent = `${percent}%`
      }
    }).then()
  }
  
  async hide() {
    if (this.isHiding) {
      return
    }

    this.isHiding = true

    await Promise.all([
      // Rotate loading element
      gsap.fromTo(this.el.content, {
        rotation: 0,        
      }, {
        rotation: 360,
        duration: 1.8,
        ease: 'expo.inOut'
      }).then(),
      
      // Hide front path
      gsap.to(this.el.front.path, {
        strokeDashoffset: this.pathLength,
        duration: 1.8,
        ease: 'expo.inOut'
      }).then(),
      
      // Hide back path
      gsap.fromTo(this.el.back.path, {
        strokeDashoffset: this.pathLength * 2,        
      }, {
        strokeDashoffset: this.pathLength,
        duration: 1.8,
        delay: 0.2,
        ease: 'expo.inOut'
      }).then()
    ])
    
    this.isHiding = false
    
    // If you want to remove the loading element, you can do so here
    // this.el.loading.parentNode.removeChild(this.el.loading)
  }
}

const loading = new Loading()
let progress = 0

loading.show()

document.body.addEventListener('click', async () => {
  if (progress >= 1) {
    return
  }

  progress += 0.25
  
  if (progress >= 1) {
    await loading.progress(progress)

    progress = 0
    loading.updateText(0)
    loading.show()
  } else {
    loading.progress(progress)
  }
})
View Compiled

External CSS

  1. https://fonts.googleapis.com/css2?family=Roboto+Mono&amp;display=swap

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js