Pull to Refresh Feed
Activity feed you pull down to refresh, with rubber-band resistance, a spinner and a new item that slides in at the top.
Preview and code
index.html
<div class="pf"><div class="cd"><div class="hd"><div><b>Activity</b><small>Pull down to check for updates</small></div><span class="lv">live</span></div><div class="vp"><div class="ind"><i></i></div><div class="ls"></div></div></div></div>
styles.css
.pf{width:100%;height:100%;min-height:380px;display:grid;place-items:center;font-family:system-ui,-apple-system,sans-serif;color:#eee;}.cd{width:340px;background:#161616;border:1px solid #2a2a2a;border-radius:20px;overflow:hidden}.hd{display:flex;justify-content:space-between;align-items:flex-start;padding:14px 16px;border-bottom:1px solid #222}.hd b{display:block;font-size:14px}.hd small{font-size:11px;color:#777}.lv{font:10px ui-monospace,monospace;color:#888;background:#222;padding:3px 7px;border-radius:99px}.vp{position:relative;height:250px;overflow:hidden;touch-action:none;cursor:grab}.ind{position:absolute;top:0;left:0;right:0;height:44px;display:grid;place-items:center;opacity:0}.ind i{width:20px;height:20px;border-radius:50%;border:2px solid #444;border-top-color:#0a84ff;box-sizing:border-box}.ind.sp i{animation:sp .7s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}.ls{position:relative;transition:transform .4s cubic-bezier(.3,1.3,.5,1)}.ls.d{transition:none}.it{display:flex;gap:10px;padding:12px 16px;border-bottom:1px solid #1f1f1f}.it.n{animation:in .5s cubic-bezier(.3,1.3,.5,1)}@keyframes in{from{opacity:0;transform:translateY(-16px)}}.ic{width:30px;height:30px;border-radius:9px;background:#222;display:grid;place-items:center;font-size:12px;color:#aaa;flex:none}.it b{display:block;font-size:12px}.it small{font-size:11px;color:#777}.tm{margin-left:auto;font-size:10px;color:#666}
script.js
(function(){var R=document.querySelector(".pf"),vp=R.querySelector(".vp"),ls=R.querySelector(".ls"),ind=R.querySelector(".ind"),L=[["⇅","Motion review approved","The pull request is ready to merge.","4m"],["✎","New component feedback","The spring feels much closer to native now.","18m"],["✓","Registry checks passed","All component files resolved successfully.","31m"],["▲","Preview deployed","Your preview is live and shareable.","1h"],["◔","Weekly summary ready","Open it to see the highlights.","2h"]],N=["Build finished","New comment on launch plan","Release notes drafted","Review requested"],k=0,busy=false;function add(a,n){var e=document.createElement("div");e.className="it"+(n?" n":"");e.innerHTML="<span class=\"ic\">"+a[0]+"</span><span><b>"+a[1]+"</b><small>"+a[2]+"</small></span><span class=\"tm\">"+a[3]+"</span>";if(n)ls.insertBefore(e,ls.firstChild);else ls.appendChild(e)}L.forEach(function(a){add(a)});var sy=null,p=0;vp.onpointerdown=function(e){if(busy)return;sy=e.clientY;vp.setPointerCapture(e.pointerId);ls.classList.add("d")};vp.onpointermove=function(e){if(sy===null)return;var dy=e.clientY-sy;if(dy<=0){p=0}else p=Math.min(110,dy*.5);ls.style.transform="translateY("+p+"px)";ind.style.opacity=Math.min(1,p/50);ind.firstChild.style.transform="rotate("+p*4+"deg)"};vp.onpointerup=function(){if(sy===null)return;sy=null;ls.classList.remove("d");if(p>=55){busy=true;ls.style.transform="translateY(44px)";ind.style.opacity=1;ind.classList.add("sp");setTimeout(function(){add([ "★",N[k%N.length],"Just now","now"],true);k++;ind.classList.remove("sp");ind.style.opacity=0;ls.style.transform="";busy=false},1100)}else{ls.style.transform="";ind.style.opacity=0}p=0}})();