Large Title

라지 타이틀

A big page title at the top shrinks as you scroll down, settling into the compact navigation bar.

Also known as: Collapsing titleCollapsing toolbar
···
html
<div class="stage"><div class="phone"><div class="notch"></div><div class="screen" id="screen">
<div class="navbar"><span id="smallTitle" class="smallTitle">Inbox</span></div>
<div class="scroller" id="scroller">
  <h1 id="bigTitle" class="bigTitle">Inbox</h1>
  <div class="item"></div><div class="item"></div><div class="item"></div>
  <div class="item"></div><div class="item"></div><div class="item"></div><div class="item"></div>
</div></div><div class="home"></div></div></div>
css
.stage{position:absolute;inset:0;display:grid;place-items:center;overflow:hidden}
.phone{position:relative;height:90vmin;max-height:640px;aspect-ratio:9/19.5;background:#08080c;
  border-radius:20%/8.6%;box-shadow:0 0 0 2px rgba(255,255,255,.08) inset,0 12px 30px rgba(0,0,0,.35)}
.phone .notch{position:absolute;left:50%;top:0;transform:translateX(-50%);width:38%;height:5.6%;
  background:#08080c;border-radius:0 0 46% 46%;z-index:80}
.phone .screen{position:absolute;top:7.6%;left:3%;right:3%;bottom:3%;border-radius:11%/4.6%;overflow:hidden;
  background:var(--surface);color:var(--fg)}
.phone .home{position:absolute;left:50%;bottom:1.1%;transform:translateX(-50%);width:32%;height:3px;
  border-radius:2px;background:rgba(255,255,255,.45);z-index:80}
.pfx{position:absolute;left:0;top:0;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;
  background:color-mix(in srgb,var(--accent) 55%,transparent);
  box-shadow:0 0 0 6px color-mix(in srgb,var(--accent) 15%,transparent);pointer-events:none;z-index:999;
  transition:opacity .2s}
.pfx.hold{width:30px;height:30px;margin:-15px 0 0 -15px;
  box-shadow:0 0 0 9px color-mix(in srgb,var(--accent) 20%,transparent);
  transition:width .5s linear,height .5s linear,margin .5s linear,box-shadow .5s linear,opacity .2s}

.navbar{position:absolute;top:0;left:0;right:0;height:11%;display:grid;place-items:center;
  background:var(--surface);border-bottom:1px solid var(--line);z-index:10}
.smallTitle{font-weight:700;font-size:clamp(7px,2.4vmin,10px);opacity:0}
.scroller{position:absolute;inset:11% 0 0;overflow-y:auto;padding:2% 7% 8%}
.bigTitle{margin:6% 0;font-weight:800;font-size:clamp(14px,7.4vmin,24px);transform-origin:left center}
.item{height:clamp(16px,10vmin,28px);border-radius:8px;background:var(--line);margin-bottom:6%}
js
const pfx=document.createElement('div');pfx.className='pfx';document.body.appendChild(pfx);
const screenEl=document.getElementById('screen');
let pfxAuto=true;
function pfxHide(){pfx.style.opacity='0';}
addEventListener('pointerdown',()=>{pfxAuto=false;pfxHide();},{capture:true});
function pfxAt(fx,fy){
  const r=screenEl.getBoundingClientRect();
  const x=r.left+r.width*fx, y=r.top+r.height*fy;
  pfx.style.transform='translate('+x+'px,'+y+'px)';
  return {x,y};
}
function pfxWalk(x0,y0,x1,y1,ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){
      if(!pfxAuto) return res();
      const p=Math.min(1,(now-start)/ms), e=1-Math.pow(1-p,3);
      pfxAt(x0+(x1-x0)*e, y0+(y1-y0)*e);
      if(p<1) requestAnimationFrame(step); else res();
    }
    requestAnimationFrame(step);
  });
}
function pfxWait(ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){ if(!pfxAuto) return res(); if(now-start<ms) requestAnimationFrame(step); else res(); }
    requestAnimationFrame(step);
  });
}

const scroller=document.getElementById('scroller'), big=document.getElementById('bigTitle'), small=document.getElementById('smallTitle');
const THRESH=38;
function apply(scrollTop){
  const p=Math.min(1,Math.max(0,scrollTop/THRESH));
  big.style.opacity=String(1-p); big.style.transform='scale('+(1-p*0.35)+')';
  small.style.opacity=String(p);
}
scroller.addEventListener('scroll',()=>apply(scroller.scrollTop));
async function autoSeq(){
  while(pfxAuto){
    const max=scroller.scrollHeight-scroller.clientHeight;
    const start=performance.now(), dur=1400;
    while(pfxAuto){
      const t=(performance.now()-start)/dur;
      if(t>=1) break;
      const st=(1-Math.cos(t*Math.PI))/2*max;
      scroller.scrollTop=st; apply(st);
      pfxAt(0.5, 0.15+((1-Math.cos(t*Math.PI))/2)*0.65);
      await new Promise(res=>requestAnimationFrame(res));
    }
    if(!pfxAuto)break;
    await pfxWait(500);
  }
}
autoSeq();

Read the scroll container's `scrollTop` on every `scroll` event and map the range 0 to a threshold (usually 40–56px) into a progress value `p`. Use `p` to shrink the large title's `font-size`/`opacity` while growing the small title's — the one that lives inside the fixed nav bar — in the opposite direction.

The trick is keeping them as **the same text in two separate elements**: the large title scrolls with the content, the small one is pinned inside the nav bar. Crossfading between the two, instead of interpolating `font-size` directly, avoids the reflow cost of a font size that keeps changing.

iOS's canonical implementation is `UINavigationBar`'s `prefersLargeTitles`; Android's Material `CollapsingToolbarLayout` plays the same role. Both share the same priority — show the current screen's name big while you're just arriving, then get out of the way once scrolling signals you're moving on.

When to use

Use it at the entry point of a top-level tab — an inbox, settings. Screens you navigate deeper into typically start with the small title only.