SCROLL / 186
Scroll-spread typography
文字を左右へほどく
文字を一つずつ分け、中心から左右へ広げます。
見本の枠内をスクロールするか、位置スライダーを動かしてください。
ひとつ試すなら
どこが変わるかを観察して、似た表現と見比べてみよう。
こんなときに
短い見出しや、印象を変えるタイポグラフィ。
仕組みと使い方
読みやすさが必要な文章とは分けて使います。見本は一つの単語を分割しています。
対応状況は使用する機能と端末により異なります。公式資料も参照してください。
表現名には通称や説明のための名称を含みます。デモと掲載コードはこの図鑑のオリジナルです。作者について
実装コードを見る
<!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 .split-word{display:flex;align-items:center;justify-content:center;gap:1px}.sample .split-word i{font:900 48px Arial;letter-spacing:-.08em;font-style:normal;display:block}.sample .split-caption{position:absolute;bottom:24px;font:8px monospace;letter-spacing:.15em;color:#777}</style><div class="sample"><div class="scroll-lab"><div class="scroll-window " tabindex="0" aria-label="見本の中を上下にスクロール。下のスライダーでも操作できます。"><div class="scroll-road"><div class="scroll-scene"><div class="split-word" aria-label="GOOD"><i>G</i><i>O</i><i>O</i><i>D</i></div><span class="split-caption">A WORD, FOUR FORMS.</span><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)=>{const a=c.reduced?0:p;[...c.root.querySelectorAll('.split-word i')].forEach((n,i)=>{const d=i-1.5;n.style.transform=`translate(${d*a*25}px,${Math.abs(d)*a*8}px) rotate(${d*a*8}deg)`;n.style.opacity=String(1-a*.2)})};const control=connectScroll(document,{reducedMotion:matchMedia('(prefers-reduced-motion:reduce)').matches},render);addEventListener('pagehide',()=>control.destroy(),{once:true});</script></html>