SCROLL / 178

Scroll zoom-out

引いて全体を見せる
近づいた画面から徐々に引き、全体像を見せます。

見本の枠内をスクロールするか、位置スライダーを動かしてください。

ラボで詳しく試す ↗

ひとつ試すなら

どこが変わるかを観察して、似た表現と見比べてみよう。

こんなときに

写真や図版の一部から、全体へ視線を導く場面。

仕組みと使い方

素材はオリジナルのSVGです。動きを抑える設定では全体像を固定表示します。

ズームzoom拡大縮小全体
実装コードを見る
<!doctype html><html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>html,body{margin:0;height:100%;min-height:300px}.sample{width:100%;height:100%;min-height:190px;box-sizing:border-box;padding:12px;display:grid;place-items:center;overflow:hidden;background:#f4f4f4;color:#242424;font-family:Arial,'Hiragino Kaku Gothic ProN',Meiryo,sans-serif}.sample *{box-sizing:border-box}.sample .scroll-lab{width:100%;max-width:430px;height:100%;min-height:0;display:grid;grid-template-rows:minmax(110px,1fr) 27px;gap:7px}.sample .scroll-window{position:relative;height:100%;min-height:110px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;border:1px solid #cfcfcf;border-radius:8px;background:#ededed;--vh:140px;scroll-behavior:auto}.sample .scroll-window:focus-visible,.sample .scroll-range:focus-visible{outline:2px solid #555;outline-offset:3px}.sample .scroll-road{height:calc(var(--vh)*4);position:relative}.sample .scroll-scene{position:sticky;top:0;height:var(--vh);overflow:hidden;width:100%;display:grid;place-items:center}.sample .scroll-control{display:flex;align-items:center;gap:9px;width:100%;font-size:10px;color:#666}.sample .scroll-range{min-width:0;flex:1;width:100%;accent-color:#333;margin:0;height:24px;touch-action:pan-y}.sample .scroll-output{font:10px monospace;min-width:32px;text-align:right}.sample .scroll-hint{position:absolute;right:10px;bottom:8px;font:8px monospace;letter-spacing:.08em;z-index:5;color:#666;background:#f3f3f3cf;padding:3px 5px;border-radius:3px;pointer-events:none}.sample .scene-title{font:900 45px Arial;letter-spacing:-.07em;line-height:.95;margin:0}.sample svg{max-width:100%}.sample button{font:inherit;touch-action:manipulation}@media(prefers-reduced-motion:reduce){.sample *{scroll-behavior:auto!important;transition:none!important;animation:none!important}}.sample .zoom-art{position:absolute;inset:0;width:100%;height:100%;max-width:none;transform-origin:60% 40%;transform:scale(1.65)}</style><div class="sample"><div class="scroll-lab"><div class="scroll-window " tabindex="0" aria-label="見本の中を上下にスクロール。下のスライダーでも操作できます。"><div class="scroll-road"><div class="scroll-scene"><svg class="zoom-art" viewBox="0 0 300 180" preserveAspectRatio="xMidYMid slice" role="img" aria-label="幾何学的な景色"><rect width="300" height="180" fill="#d8d8d8"/><circle cx="205" cy="52" r="32" fill="#6c6c6c"/><path d="M0 180V140L90 50 185 150 250 85 300 140V180Z" fill="#a4a4a4"/><path d="M0 180V152L70 105 173 180Z" fill="#393939"/><text x="20" y="30" font-family="Arial" font-weight="bold" font-size="15" fill="#404040">THE WHOLE PICTURE</text></svg><span class="scroll-hint">SCROLL ↕</span></div></div></div><label class="scroll-control"><span>位置</span><input class="scroll-range" type="range" min="0" max="100" value="0" aria-label="見本のスクロール位置"><output class="scroll-output">0%</output></label></div></div><script>const connectScroll=function connectScroll(root, options={}, render){
 const port=root.querySelector('.scroll-window'),range=root.querySelector('.scroll-range'),output=root.querySelector('.scroll-output');
 const abort=new AbortController(),animations=new Set(),cleanups=[];
 const osReduced=globalThis.matchMedia?.('(prefers-reduced-motion:reduce)').matches||false;
 let alive=true,reduced=options.reducedMotion??osReduced,frame=0,oldTop=0,lastTime=performance.now();
 const state={};const clamp=(v,min=0,max=1)=>Math.min(max,Math.max(min,v));
 const ctx={root,port,state,clamp,get reduced(){return reduced},get alive(){return alive},width:0,height:0,delta:0,velocity:0,progress:0,
  track(a){animations.add(a);a.finished.then(()=>animations.delete(a),()=>animations.delete(a));return a},
  on(node,event,fn,opts={}){node.addEventListener(event,fn,{...opts,signal:abort.signal})},cleanup(fn){cleanups.push(fn)}
 };
 function update(){frame=0;if(!alive)return;const now=performance.now(),top=Math.max(0,port.scrollTop),max=Math.max(0,port.scrollHeight-port.clientHeight),p=max?clamp(top/max):0;
  ctx.width=port.clientWidth;ctx.height=port.clientHeight;ctx.delta=top-oldTop;ctx.velocity=clamp(ctx.delta/Math.max(16,now-lastTime),-3,3);ctx.progress=p;
  oldTop=top;lastTime=now;range.value=String(Math.round(p*100));output.textContent=Math.round(p*100)+'%';port.classList.toggle('motion-reduced',reduced);render(p,ctx);
 }
 function queue(){if(!frame)frame=requestAnimationFrame(update)}
 function resize(){if(!alive)return;port.style.setProperty('--vh',Math.max(110,port.clientHeight)+'px');queue()}
 port.addEventListener('scroll',queue,{passive:true,signal:abort.signal});
 range.addEventListener('input',()=>{port.scrollTop=Number(range.value)/100*Math.max(0,port.scrollHeight-port.clientHeight);queue()},{signal:abort.signal});
 let observer;if('ResizeObserver'in globalThis){observer=new ResizeObserver(resize);observer.observe(port)}
 resize();cancelAnimationFrame(frame);frame=0;update();
 return{destroy(){if(!alive)return;alive=false;abort.abort();cancelAnimationFrame(frame);observer?.disconnect();animations.forEach(a=>a.cancel());animations.clear();cleanups.forEach(fn=>fn())},
 setAuto(value){reduced=!value||osReduced;if(reduced){animations.forEach(a=>a.cancel());animations.clear()}update()}};
};const render=(p,c)=>{c.root.querySelector('.zoom-art').style.transform=`scale(${c.reduced?1:1.65-p*.65})`};const control=connectScroll(document,{reducedMotion:matchMedia('(prefers-reduced-motion:reduce)').matches},render);addEventListener('pagehide',()=>control.destroy(),{once:true});</script></html>