<header id="refresh">
  <svg viewBox="0 0 24 24" width="24" height="24">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
  </svg>
  <span>Pull to refresh</span> 
</header>
<main id="refresh-main">
  <h1>Header</h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempora laborum illo autem asperiores. Numquam voluptate facilis odit impedit non autem magni architecto, placeat voluptatum dolorem nemo doloremque velit, iure id.</p> 
</main>
@keyframes rotate-in {
	to {
		transform: rotateZ(.5turn);
	}
}

@keyframes fade-out {
	to {
		opacity: 0;
	}
}

#refresh > svg {
  animation: linear rotate-in;
  animation-timeline: view();
  animation-range: exit 0% exit 100%;
}

#refresh > span {
  animation: linear fade-out;
  animation-timeline: view();
  animation-range: exit -200% exit 100%;
}


/* DEMO SUPPORT */
#refresh {
  block-size: 150px;
  inline-size: 100%;
  background: hsl(0 0% 50% / 10%);
  display: grid;
  gap: 1ch;
  align-content: center;
  justify-items: center;
  position: relative;
}

#refresh::before {
  content: "";
  position: absolute;
  inset: 0;
  block-size: 10px;
  animation: delayed-snap-point 2ms forwards;
}

#refresh::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  block-size: 5px;
  background: deeppink;
  opacity: 0;
}

#refresh[loading-state="loading"]::after {
  animation: indeterminate-loading 1s ease infinite;
}

@keyframes delayed-snap-point {
  to { scroll-snap-align: start }
}

@keyframes indeterminate-loading {
  50% { opacity: 1 }
}

#refresh > svg {
  --size: 4ch;
  fill: none;
  stroke: currentColor;
  inline-size: var(--size);
  block-size: var(--size);
}

html {
  scroll-snap-type: y mandatory;
  overscroll-behavior: contain;
  scroll-behavior: smooth;
  color-scheme: dark light;
}

main {
  padding: 2ch;
  /*  the only child with snap alignment is "scroll start"  */
  scroll-snap-align: start;
  /*  it's not "toss to refresh"  */
  scroll-snap-stop: always;
  min-block-size: 200vh; 
}

body {
  margin: 0;
  padding: 0;
  font-family: system-ui;
  display: grid;
  justify-items: center;
  overscroll-behavior: none;
}

p {
  max-inline-size: 40ch;
  font-size: 1.25rem;
  font-weight: 200;
  line-height: 1.5;
}
import {scrollend} from 'https://cdn.jsdelivr.net/gh/argyleink/scrollyfills@latest/dist/scrollyfills.modern.js'

const ptr_scrollport = document.querySelector('html')
const ptr = document.querySelector('#refresh')
const main = document.querySelector('#refresh-main')

const determinePTR = event => {
  if (event.target.scrollTop <= 0) {
    // fetch()
    ptr.querySelector('span').textContent = 'refreshing...'
    ptr.setAttribute('loading-state', 'loading')
    
    // sim response
    setTimeout(() => {
      ptr.querySelector('span').textContent = 'done!'
      
      setTimeout(() => {
        ptr.removeAttribute('loading-state')
        main.scrollIntoView({ behavior: 'smooth' })

        window.addEventListener('scrollend', e => {
          ptr.querySelector('span').textContent = 'Pull to refresh'
        }, {once: true})
      }, 500)
    }, 2000)
  }
}

window.addEventListener('scrollend', e => {
  determinePTR({target: ptr_scrollport})
})
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.