INTERACTION / 290
Multimodal feedback
色だけに、意味を預けない
記号・文章・読み上げ通知を重ね、状態を複数の方法で伝えます。
見本の中を操作して変化を試してください。
ひとつ試すなら
色以外でも意味を伝える
こんなときに
実際の画面で、操作と情報の関係を設計するとき。
仕組みと使い方
見本のボタンを操作し、変わる場所と、変わらない場所を見比べてください。
標準機能。端末ごとの検証状況は収録マップを参照。
表現名には通称や説明のための名称を含みます。デモと掲載コードはこの図鑑のオリジナルです。作者について
実装コードを見る
<!doctype html><html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{margin:0;min-height:330px}.sample{width:100%;height:100%;min-height:330px;padding:12px!important;display:flex!important;flex-direction:column;justify-content:center;align-items:center;gap:8px!important;background:#f0efeb;color:#232323;font-family:Arial,Meiryo,sans-serif;overflow:hidden;box-sizing:border-box}.sample *{box-sizing:border-box}.lab-scene{width:100%;max-width:430px;height:140px;position:relative;overflow:hidden;border-bottom:1px solid #ccc}.lab-scene:has(canvas){max-width:320px;height:auto;aspect-ratio:16/9;flex-shrink:0}.lab-canvas{display:block;width:100%;height:100%!important;outline-offset:-3px}.lab-canvas:focus-visible{outline:2px solid #222}.lab-controls{width:100%;max-width:390px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 14px}.lab-controls label{display:grid;grid-template-columns:1fr auto;gap:2px;font-size:11px;min-width:0}.lab-controls input{grid-column:1/-1;width:100%;min-width:0;margin:0;accent-color:#333;height:30px}.lab-controls output{font-variant-numeric:tabular-nums}.lab-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center}.sample .lab-actions button{color:inherit;font:11px Arial,Meiryo,sans-serif;border:1px solid #aaa;border-radius:99px;background:transparent;min-height:44px;padding:8px 12px;cursor:pointer}.lab-status{font-size:10px;line-height:1.4;min-height:14px;margin:0;text-align:center}.lab-note{font-size:9px;line-height:1.3;color:#666;margin:0;text-align:center}.lab-actions button:focus-visible{outline:2px solid #222;outline-offset:2px}@media(prefers-reduced-motion:reduce){.sample *{animation-play-state:paused!important;transition:none!important}}.lab-scene{height:180px;max-width:450px;overflow:auto;border:1px solid #d6d4ce;background:#f8f7f3;border-radius:3px;padding:14px;container-type:inline-size}.lab-scene button,.lab-scene input,.lab-scene select{font:inherit;color:inherit;border:1px solid #aaa;border-radius:3px;padding:9px;background:transparent;min-height:40px;max-width:100%}.lab-scene button{cursor:pointer}.lab-scene button:focus-visible,.lab-scene input:focus-visible{outline:2px solid #222;outline-offset:2px}.lab-scene p{margin:8px 0;font-size:12px;line-height:1.6}.lab-scene h3{font-size:23px;letter-spacing:-1px;margin:4px 0}.lab-scene small{font:10px monospace;letter-spacing:.1em}.lab-scene .row{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.lab-scene .ink{background:#242424;color:#fff}.lab-scene .notice{padding:10px;background:#e4e2da}.lab-scene .tiny{font-size:10px}.lab-scene [hidden]{display:none!important}.lab-scene dialog{border:1px solid #333;padding:22px;max-width:min(290px,85vw);background:#f8f7f3;color:#222}.lab-scene dialog::backdrop{background:#0008}.lab-scene .popover-box{border:1px solid #333;padding:14px;background:#f8f7f3;color:#222;max-width:220px}.lab-scene .stage-card{border:1px solid #bbb;padding:12px}.lab-scene .dot{display:block;width:28px;height:28px;border-radius:50%;background:#292929}.lab-scene .track{height:50px;position:relative;padding:10px}.lab-scene .strip{display:flex;gap:8px}.lab-scene .strip>i{display:grid;place-items:center;width:50px;height:50px;background:#292929;color:#fff;font-style:normal}.lab-scene .measure{container-type:inline-size;width:100%;max-width:100%;border:1px dashed #aaa;padding:8px}.lab-scene .layout-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}.lab-scene .layout-grid>div{padding:8px;background:#e2e0d9}.lab-scene .case-cover{background:radial-gradient(ellipse at 50% 35%,#ddd 0 20%,#333 21% 35%,#b6b3aa 36%);height:68px}.lab-scene .case-head{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid;padding-bottom:8px;margin-bottom:10px}.lab-scene .case-head b{font-size:18px}.lab-scene .case-layout{display:grid;grid-template-columns:1fr 1fr;gap:14px}@container(max-width:280px){.lab-scene .case-layout{grid-template-columns:1fr}}</style><div class="sample"><div class="lab-scene"><h3>Every signal counts.</h3><p data-message role="status">✓ 保存しました。</p></div><div class="lab-controls"></div><div class="lab-actions"><button data-play>停止</button><button data-reset>試してみる</button></div><p class="lab-status" role="status"></p></div><script>const lab=(function runDomLab(root,cfg,setup){
const abort=new AbortController(),values=Object.fromEntries(cfg.controls.map(c=>[c.key,c.value])),mq=matchMedia('(prefers-reduced-motion:reduce)');let auto=!mq.matches;
const on=(el,event,fn)=>el.addEventListener(event,fn,{signal:abort.signal});
const api=setup({root,values,kind:cfg.kind,on,reduced:mq.matches})||{};
for(const input of root.querySelectorAll('[data-key]'))on(input,'input',()=>{values[input.dataset.key]=Number(input.value);input.closest('label').querySelector('output').textContent=input.value;api.update?.()});
const play=root.querySelector('[data-play]');on(play,'click',()=>{auto=!auto;api.setAuto?.(auto);for(const a of root.querySelector('.lab-scene').getAnimations({subtree:true}))auto?a.play():a.pause();play.textContent=auto?'停止':'再生';play.setAttribute('aria-pressed',String(!auto))});
const reset=root.querySelector('[data-reset]');on(reset,'click',()=>api.action?.());
on(document,'visibilitychange',()=>api.setAuto?.(auto&&!document.hidden));
api.update?.();if(cfg.static)play.hidden=true;
return {setAuto(on){auto=on&&!mq.matches;api.setAuto?.(auto);for(const a of root.querySelector('.lab-scene').getAnimations({subtree:true}))auto?a.play():a.pause();play.textContent=auto?'停止':'再生'},destroy(){abort.abort();api.destroy?.()}};
})(document,{"kind":"access-feedback","controls":[],"static":true,"action":"試してみる"},function setup({root,values:v,kind,on,reduced}){
const $=s=>root.querySelector(s),$$=s=>[...root.querySelectorAll(s)],stage=$('.lab-scene'),status=$('.lab-status');let timers=[],animations=[],stopped=false,phase=0,dead=false;
const say=s=>status.textContent=s,wait=(fn,t)=>{const id=setTimeout(()=>{if(!dead)fn()},t);timers.push(id)},animate=(el,frames,options={})=>{if(reduced||stopped)return null;const a=el.animate(frames,{duration:v.duration||650,fill:'both',easing:'cubic-bezier(.2,.8,.2,1)',...options});animations.push(a);return a};
const apply=()=>{stage.style.setProperty('--width',v.width+'%');stage.style.setProperty('--scale',v.size);if($('.measure'))$('.measure').style.width=(v.width||100)+'%';};
let action=()=>{};
if(kind==='ui-dialog'){const d=$('dialog'),trigger=$('[data-open]');on(trigger,'click',()=>{d.showModal();say('背景の操作を止め、フォーカスをダイアログへ')});on($('[data-close]'),'click',()=>d.close());on(d,'close',()=>{trigger.focus();say('元のボタンへフォーカスを戻しました')});action=()=>{if(d.open)d.close();else trigger.click()};}
if(kind==='ui-popover'||kind==='ui-tooltip'||kind==='ui-menu'){
const box=$('.popover-box'),button=$('[data-open]');let open=false;const native=typeof box.showPopover==='function';
const show=value=>{open=value;button.setAttribute('aria-expanded',String(value));if(native){try{value?box.showPopover():box.hidePopover()}catch{}}else box.hidden=!value;say(native?'Popover API · 外側のクリック/Escapeで閉じます':'この環境では通常の開閉表示で代替します')};
if(!native){box.removeAttribute('popover');box.hidden=true}on(button,'click',()=>show(!open));on(box,'toggle',e=>{open=e.newState==='open';button.setAttribute('aria-expanded',String(open))});on(stage,'keydown',e=>{if(e.key==='Escape')show(false)});
if(kind==='ui-tooltip'){on(button,'focus',()=>show(true));on(button,'blur',()=>show(false));on(button,'pointerenter',()=>show(true));on(button,'pointerleave',()=>show(false));}
for(const b of box.querySelectorAll('button'))on(b,'click',()=>{show(false);say(b.textContent+'を選択');button.focus()});action=()=>show(!open);
}
if(kind==='ui-validation'||kind==='case-form'){
const f=$('form'),input=$('input[type=email]'),msg=$('[data-message]');on(input,'input',()=>{input.removeAttribute('aria-invalid');msg.textContent=''});
on(f,'submit',e=>{e.preventDefault();if(!input.validity.valid){input.setAttribute('aria-invalid','true');msg.textContent='メールアドレスを確認してください。例 hello@example.com';input.focus();say('入力欄と説明を関連付けています');return}input.setAttribute('aria-invalid','false');msg.textContent='入力を確認しました。この見本は外部に送信しません。';say('✓ 完了');});action=()=>f.requestSubmit();
}
if(kind==='ui-submit'){
const b=$('[data-submit]'),out=$('[data-message]'),mode=$('select');on(b,'click',()=>{if(b.disabled)return;b.disabled=true;b.setAttribute('aria-busy','true');out.textContent='処理中…';say('操作の重複を防止');wait(()=>{b.disabled=false;b.removeAttribute('aria-busy');out.textContent=mode.value==='success'?'✓ 完了しました':'! 接続できませんでした。もう一度試せます。';say(mode.value==='success'?'完了状態':'再試行できる状態')},900)});action=()=>b.click();
}
if(kind==='ui-empty'){let items=0;action=()=>{items=items?0:3;$('[data-empty]').hidden=!!items;$('[data-items]').hidden=!items;say(items?'内容がある状態':'次の操作を案内する空の状態')};on($('[data-add]'),'click',action);}
if(kind==='ui-undo'){const item=$('[data-item]'),undo=$('[data-undo]');on($('[data-delete]'),'click',()=>{item.hidden=true;undo.hidden=false;say('項目を取り除きました。取り消せます。');undo.focus()});on(undo,'click',()=>{item.hidden=false;undo.hidden=true;$('[data-delete]').focus();say('元に戻しました')});action=()=>{item.hidden?undo.click():$('[data-delete]').click()};}
if(kind==='layout-input'){const pointer=matchMedia('(pointer:coarse)');$('[data-mode]').textContent=pointer.matches?'端末の入力方式 · タッチ中心':'端末の入力方式 · 精密なポインター';action=()=>{stage.classList.toggle('coarse');say(stage.classList.contains('coarse')?'タッチ向けの大きな操作領域':'マウス向けのコンパクトな操作領域')};}
if(kind==='rhythm-timeline'){action=()=>{animations.forEach(a=>a.cancel());$$('.strip i').forEach((el,i)=>animate(el,[{opacity:0,transform:'translateY(25px)'},{opacity:1,transform:'none'}],{delay:i*v.delay,fill:'both'}));say('位置・透明度・時間差を一つのシーケンスで設計')};}
if(kind==='rhythm-interrupt'){const dot=$('.dot');action=()=>{const current=getComputedStyle(dot).transform;animations.forEach(a=>a.cancel());phase=1-phase;const target=phase?Math.max(0,stage.clientWidth-85):0;animate(dot,[{transform:current},{transform:`translateX(${target}px)`}],{duration:v.duration});if(reduced||stopped)dot.style.transform=`translateX(${target}px)`;say('連打すると、現在位置から新しい目的地へ向かいます')};}
if(kind==='rhythm-enter-exit'){const card=$('.stage-card');action=()=>{animations.forEach(a=>a.cancel());phase=1-phase;card.style.opacity=phase?'0':'1';animate(card,phase?[{opacity:1,transform:'none'},{opacity:0,transform:'translateY(-15px)'}]:[{opacity:0,transform:'translateY(25px)'},{opacity:1,transform:'none'}],{duration:phase?v.duration*.55:v.duration});say(phase?'退場は短く、上へ':'登場は少し長く、下から')};}
if(kind==='transition-native'){const change=()=>{phase=1-phase;stage.classList.toggle('expanded',!!phase);$('[data-title]').textContent=phase?'A closer look.':'An object.'};action=()=>{if(document.startViewTransition&&!reduced&&!stopped){document.startViewTransition(change);say('View Transition API で状態を更新')}else{change();say('通常の切り替えで表示しています')}};}
if(kind==='scroll-native'){const port=$('.native-scroll'),bar=$('.native-progress'),native=CSS.supports('animation-timeline','scroll()');if(!native){on(port,'scroll',()=>{const p=port.scrollTop/Math.max(1,port.scrollHeight-port.clientHeight);bar.style.transform=`scaleX(${p})`})}say(native?'CSS scroll() でスクロールと進行度を接続':'JavaScriptで進行度を代替表示');action=()=>{port.scrollTop=port.scrollTop>20?0:port.scrollHeight};}
if(kind==='access-motion'){const card=$('.stage-card');action=()=>{phase=1-phase;card.textContent=phase?'✓ Saved':'Draft';animations.forEach(a=>a.cancel());if($('input[type=checkbox]').checked||reduced){say('動きは使わず、文字と状態の違いで伝えます')}else{animate(card,[{transform:'translateY(20px)',opacity:0},{transform:'none',opacity:1}]);say('動きと文字で状態を伝えます')}};}
if(kind==='access-feedback'){action=()=>{phase=1-phase;$('[data-message]').textContent=phase?'✓ 保存しました。':'! 入力内容を確認してください。';say('色に加えて、記号と文章でも状態を伝える')};}
if(kind==='case-product'){const b=$('[data-cart]');on(b,'click',()=>{const n=Number(b.dataset.count||0)+1;b.dataset.count=n;b.textContent=`バッグに追加済み · ${n}`;say('✓ オブジェクトを追加しました');animate($('.case-cover'),[{transform:'scale(.95)'},{transform:'scale(1)'}])});action=()=>b.click();}
if(kind==='case-editorial'){const port=$('.story-port');on(port,'scroll',()=>{$('[data-progress]').style.width=(port.scrollTop/Math.max(1,port.scrollHeight-port.clientHeight)*100)+'%'});action=()=>{port.scrollTop=port.scrollTop?0:port.scrollHeight};}
if(kind==='case-gallery'){const hero=$('.case-cover');$$('[data-image]').forEach(b=>on(b,'click',()=>{const next=b.dataset.image;hero.style.background=`linear-gradient(${Number(next)*60}deg,#111,#c7c3b9)`;$$('[data-image]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));say('作品 '+next+' を選択');animate(hero,[{opacity:.2},{opacity:1}])}));action=()=>{$$('[data-image]')[phase++%3].click()};}
return{update(){apply();if(kind==='layout-fluid')say('枠を縮めて、文字と余白の変化を観察');if(kind==='layout-subgrid')say(CSS.supports('grid-template-columns','subgrid')?'Subgridで内側と外側の列を共有':'非対応環境では独立したGridで表示');},action,setAuto(auto){stopped=!auto;animations.forEach(a=>auto?a.play():a.pause())},destroy(){dead=true;timers.forEach(clearTimeout);animations.forEach(a=>a.cancel());const d=$('dialog');if(d?.open)d.close();for(const p of $$('[popover]')){try{p.hidePopover()}catch{}}}};
});addEventListener('pagehide',()=>lab.destroy(),{once:true});</script></html>