GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

タッチパネル 注文 画面のUI 9選 — 指の大きさで作る

タッチパネル 注文 の画面は、立ったままの客が指一本で最後まで進めるように組む画面です。ここに並べた九つは、待つ・選ぶ・迷う・受け取るの流れをそのまま九枚に割り、押す枠を先に指の大きさで決めてから中身を詰めました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

九つの並びは機能の派手さではなく、店の端末の前で一人の客がたどる順番です。誰もいない待機画面(01)から始まり、言語を選び(02)、左に立てた縦のレールで分類を選び(03)、タイルからメニューを選びます(04)。その先は迷う時間で、サイズと温度を二つの歩みに分ける大きなシート(05)、単品かセットかを尋ねる枠(06)と続きます。最後の三枚は受け取りと後始末で、番号板(07)、放置したときに最初へ戻す仕組み(08)、車いすの目線に合わせて全部を下げる低い画面(09)で締めます。ウェブの買い物かごと支払いは軸がまるで違うので決済画面 デザイン 9選 — カートから支払いまでコピペOKに分けてあり、押した瞬間の手応えだけを見たいときはCSS タップ エフェクト 9選 — 一行でコピペOKのほうが近い話です。同じinteraction柱で指の代わりにマウスが主役になるとマウス 追従 アニメーション css で作る3D場面9選になり、01 の待機画面にキャラクターを立たせたいときはcss キャラクター アニメーション 9選に九つの表情があります。

01待機画面

画面で起きること

誰も触らないあいだは三枚の広告が steps(1, end) のカットで切り替わり、下の残り時間バーが scaleX で三度伸びます。画面を押すと広告が止まり、幅 400 × 高さ 206px の注文画面が開いて、フォーカスは見出しへ移ります。

待機画面
新しいタブで開く

01 · 待機画面 — 計測 累積面積 29.122% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

steps(1, end)scaleXanimation-delay

コードのどこを見るか

プレビューは累積面積 29.122%・強度 149.3 を記録し、動いたフレームは 23/23 で、全部のコマが動きました。

// 待機画面は「誰も触らないあいだ自分で回る画面」です。手が触れた瞬間に広告を止め、注文画面へ渡します。
// プレビューのループ(.is-demo)は CSS のキーフレームが回し、人が押したらここでループを外して画面を変えます。
function start() {
  ka.classList.remove('is-demo');
  attract.hidden = true;
  order.hidden = false;
  head.focus({ preventScroll: true });
}

function home() {
  order.hidden = true;
  attract.hidden = false;
  attract.focus({ preventScroll: true });
}

02言語を選ぶ最初の画面

何が見えるか

四つの言語ボタンが順に浮かび、押した枠だけが aria-pressed="true" になって枠線が 2px から 4px に太くなり、チェックが不透明度 1 で現れます。下の案内一行も同じ瞬間にその言語の文へ入れ替わります。

言語を選ぶ最初の画面
新しいタブで開く

02 · 言語を選ぶ最初の画面 — 計測 累積面積 35.997% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-pressedanimation-fill-modetranslateY

押した後に残るもの

どの枠を押しても aria-pressed が true の枠はつねにちょうど一つで、言語名だけは各国語の表記(한국어・English・日本語・Tiếng Việt)のまま翻訳しません(累積面積 35.997%・強度 103.6・15/23)。

// 言語選択の最初の画面。選んだ枠だけが aria-pressed="true" になり、下の案内一行がその言語の文に変わります。
// プレビューのループ(.is-demo)は CSS のキーフレームが回します。手が触れた瞬間にループを外し、本当の選択に渡します。
function klPick(btn) {
  kl.classList.remove('is-demo');
  for (var i = 0; i < klOpts.length; i++) {
    klOpts[i].setAttribute('aria-pressed', klOpts[i] === btn ? 'true' : 'false');
  }
  var text = LANG_HINT[btn.getAttribute('data-lang')];
  if (text) { klHint.textContent = text; }
}

// <button> は Enter・Space をブラウザーが click に変えてくれます — キーのハンドラーを別に置きません。
for (var i = 0; i < klOpts.length; i++) {
  klOpts[i].addEventListener('click', function (e) { klPick(e.currentTarget); });
}

03縦のカテゴリーレール

券売機 ui と同じ縦の置きかた

左のレールは role="tablist" に aria-orientation="vertical" を付けた縦のタブで、選択ブロックが枠の上を滑って移動し、右のメニュー枠が同じ拍子で入れ替わります。動くのは細いブロックと数行の文字だけなので、累積面積は 4.647% に収まり、強度は 178.4 と九つでいちばん高い数字になりました。

縦のカテゴリーレール
新しいタブで開く

03 · 縦のカテゴリーレール — 計測 累積面積 4.647% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-selectedaria-orientationtranslateY

キーボードでの動き

ArrowUp・ArrowDown・Home・End を全部実測したところ、選択ブロックの top はどの枠でもその上端よりちょうど 4px 上(43 → 150 → 204 → 96 → 43)に立ち、動いたフレームは 9/23 でした。

// 縦のカテゴリーレール。選んだ枠は data-sel 一か所で示し、選択ブロックの位置と右のパネルは
// その値ひとつを見て CSS が描きます(プレビューのループ .is-demo は同じ場所をキーフレームで回します)。
// レールは role="tablist" aria-orientation="vertical" なので、上下の矢印が標準の操作です。
function krSelect(idx, moveFocus) {
  kr.classList.remove('is-demo');
  kr.setAttribute('data-sel', String(idx + 1));
  for (var i = 0; i < krTabs.length; i++) {
    var on = i === idx;
    krTabs[i].setAttribute('aria-selected', on ? 'true' : 'false');
    // ロービングタブインデックス — レール全体が Tab 一回で入り、中では矢印で移ります
    krTabs[i].setAttribute('tabindex', on ? '0' : '-1');
  }
  if (moveFocus) { krTabs[idx].focus({ preventScroll: true }); }
}

04メニュータイルと売り切れ枠

売り切れ枠の振る舞い

六枚の大きなタイルのうち一枚だけが薄くなり、斜めの帯と売り切れの文字が重なります。その枠はクリックでも Enter でも Space でもカゴの点数が 2 のまま動かず、それでいて Tab では飛ばされずフォーカスが当たります。

メニュータイルと売り切れ枠
新しいタブで開く

04 · メニュータイルと売り切れ枠 — 計測 累積面積 18.788% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-disabledrepeating-linear-gradientanimation-delay

コードの一行

disabled ではなく aria-disabled を選んだのはそのためで、押せない理由を読み上げる機会を残したまま、カゴに入れる処理だけを関数の先頭一行で止めています(累積面積 18.788%・強度 47.1・23/23)。

var BAG_LABEL = 'カゴに{n}点';
var picked = 0;

// 売り切れ枠は disabled 属性ではなく aria-disabled で示します。disabled を付けると Tab がその枠を飛ばし、
// 「なぜ押せないのか」を読み上げる機会が消えます — フォーカスは残し、カゴに入れる処理だけをここの一行で止めます。
function add(tile) {
  if (tile.getAttribute('aria-disabled') === 'true') return;
  mt.classList.remove('is-demo');
  picked += 1;
  bag.textContent = BAG_LABEL.replace('{n}', picked);
  tile.classList.add('is-tapped');
  setTimeout(function () { tile.classList.remove('is-tapped'); }, 180);
}

05段階で選ぶ大きなシート

二つの歩みの分けかた

大きなシートがサイズと温度を二つの歩みに分けて見せ、歩みが進むと上の段の表示が一つ埋まり、戻るボタンと次へボタンの disabled が入れ替わります。どちらの歩みでも aria-checked="true" の枠はちょうど一つだけです。

段階で選ぶ大きなシート
新しいタブで開く

05 · 段階で選ぶ大きなシート — 計測 累積面積 14.037% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-checkedsteps(1, end)translateX

戻ったときに残るもの

サイズを「L」にしてから次へ進み、温度を「アイス」に変えてから戻っても、サイズは選んだまま残りました(累積面積 14.037%・強度 52.2・17/23)。

function go(to) {
  ko.classList.remove('is-demo');
  step = Math.max(0, Math.min(panes.length - 1, to));
  paint();
  var first = panes[step].querySelector('[aria-checked="true"]') || panes[step].querySelector('.ko__opt');
  if (first) { first.focus({ preventScroll: true }); }
}

function pick(group, btn) {
  ko.classList.remove('is-demo');
  var opts = btn.parentNode.querySelectorAll('.ko__opt');
  for (var i = 0; i < opts.length; i++) {
    var on = opts[i] === btn;
    opts[i].setAttribute('aria-checked', on ? 'true' : 'false');
    opts[i].classList.toggle('is-on', on);
    opts[i].tabIndex = on ? 0 : -1;
  }
  picks[group] = btn.getAttribute('data-v');
}

06セットのおすすめ枠

二つのボタンの大きさ

単品を選んだ後にセットのカードが跳ね上がり、差額の数字が枠の中を回って上がります。「セットにする」と「単品のまま」は同じ 1fr の列に width:100% で立つので、実測した幅の差は 0px(153 × 42px、画面幅 320px では 133 × 37px)でした。

セットのおすすめ枠
新しいタブで開く

06 · セットのおすすめ枠 — 計測 累積面積 34.771% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

overflow: hiddentranslateYaria-live

合計が変わる場所

セットのボタンを押すと注文の行と合計が 4,500円 から 6,000円(+1,500)に変わり、カードだけが閉じて 0 × 0 になります(累積面積 34.771%・強度 123.9・17/23)。

// おすすめ枠の主張は「断るボタンを小さくしない」ことです。二つのボタンは同じ 1fr の列に width:100% で立つので、
// どちらを選んでも指の当たる広さが同じです。選んだ後はカードだけが閉じ、注文の行と合計は残ります。
var SUM_LABEL = '合計 {n}円';
var SINGLE_LINE = 'アメリカーノ 1点';
var COMBO_LINE = 'アメリカーノセット 1点';
var BASE = 4500;
var DIFF = 1500;

function money(n) {
  return String(n).replace(/\B(?=(\d{3})+$)/g, ',');
}

function settle(add) {
  ku.classList.remove('is-demo');
  var total = BASE + (add ? DIFF : 0);
  orderLine.textContent = add ? COMBO_LINE : SINGLE_LINE;
  sum.textContent = SUM_LABEL.replace('{n}', money(total));
  sheet.hidden = true;
  sum.focus({ preventScroll: true });
}

07呼び出し番号板

番号が渡るとき

A12 を押すと準備中の列が [A12, A13, A14] から [A13, A14] に減り、受け取りの列が [A09] から [A12, A09] に増えて、aria-live が読み上げる文も一緒に変わります。残った番号は空いた場所を詰めて上に上がり、フォーカスは次の番号 A13 へ移ります。

呼び出し番号板
新しいタブで開く

07 · 呼び出し番号板 — 計測 累積面積 31.229% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateXaria-livesteps(1, end)

溢れを止める一行

受け取りの列が四枠に伸びたとき 320 × 200 の舞台で文書の高さが 239px まで溢れたので、MAX_READY を 3 に決めて古い枠から捨てています(累積面積 31.229%・強度 114.4・17/23)。

var MAX_READY = 3;

// 「準備中」の番号を押すと(Enter も同じ — ネイティブの button)、その枠が「できました」の列の先頭へ移ります。
// 移った先に aria-live="polite" が付いているので、画面が見えない人も新しく出た番号を聞けます。
// プレビューのループ(.is-demo)は CSS のキーフレーム、こちらは実際の DOM 移動なので、二つは別のコードです。
making.addEventListener('click', function (e) {
  var btn = e.target.closest('.kb__num');
  if (!btn || btn.disabled) return;
  kb.classList.remove('is-demo');
  var row = btn.parentNode;
  btn.className = 'kb__num kb__num--ready';
  btn.disabled = true;
  ready.insertBefore(row, ready.firstChild);
  // 呼び出し板は直近 MAX_READY 件だけを残します。無限に積むと枠が画面の外へ伸びます
  // (298-07 実測: 四枠になった途端 320×200 で文書の高さが 239px — 操作前しか測らない BD9 はこれを見られません)
  while (ready.children.length > MAX_READY) ready.removeChild(ready.lastElementChild);
  var next = making.querySelector('.kb__num');
  if (next) next.focus({ preventScroll: true });
});

08手を離すと最初へ

残り時間の見せかた

リングは conic-gradient 一枚で、角度は @property に inherits: false で登録した --sweep が動かし、数字は 0 から 20 までを縦に積んだ列を窓ひとつでのぞく作りです。

手を離すと最初へ
新しいタブで開く

08 · 手を離すと最初へ — 計測 累積面積 2.486% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@propertyconic-gradientsteps(1, end)

実測した値

3.2 秒放置すると残り秒が 20 から 17、リングが 360deg から 306deg、数字の列が translateY 0 から -96px へ動き、0 になると案内文とともに最初へ戻りました。動く面積は 2.486% と九つでいちばん小さいのに、強度は 177.9 まで上がっています(18/23)。

function paint() {
  ring.style.setProperty('--sweep', (360 * left / TOTAL) + 'deg');
  nums.style.setProperty('--left', String(left));
  ring.setAttribute('aria-label', RING_LABEL.replace('{n}', String(left)));
}

function reset(text) {
  left = TOTAL;
  note.textContent = text;
  paint();
}

function tick() {
  left = left - 1;
  if (left <= 0) { reset(NOTE_RESET); return; }   // 尽きたら選んだ内容を捨てて最初の画面の状態へ
  paint();
}

function touch() {
  kt.classList.remove('is-demo');                 // 人が触れたらプレビューのループを外します
  reset(NOTE_IDLE);
}

09低い画面モード

タッチパネル 注文 高齢者 にも効く文字の段

切り替えボタンを押すとボタンのまとまりが top 64px から 97.6px へ 33.6px 沈み、ラベルは 13px から 16px へ一段階大きくなります(累積面積 20.542%・強度 98.8・15/23)。

低い画面モード
新しいタブで開く

09 · 低い画面モード — 計測 累積面積 20.542% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-pressedtranslateYclamp

枠の高さは変えない

同じ操作でボタンの高さは 36/36/36px のまま動かず、aria-pressed だけが false から true に変わります。目線を下げることと押す枠を小さくすることは別の話だからです。

$drop: clamp(20px, 7vw, 36px);              // まとまりが降りる距離 = まとまりの下に空けておいた場所
$btn-h: clamp(24px, 8vw, 36px);             // 押す枠の高さ。モードが変わってもこの値は変わりません
$label-sm: clamp(11px, 3.2vw, 13px);        // 基本の文字
$label-lg: clamp(13px, 3.8vw, 16px);        // 低い画面モードの一段階大きい文字
.lr__group {
  display: grid; gap: $sp-2; margin: 0; padding: 0; list-style: none;
  transform: translateY(0); transition: transform $dur-base $easing;
}
.lr.is-low .lr__group { transform: translateY($drop); }

.lr__btn {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: $btn-h; padding: $sp-1 $sp-2; cursor: pointer;
  border: 2px solid $subject-ink; border-radius: $r-control;
  background: $stage-paper; color: $subject-ink; font-family: inherit;
}
.lr__btn:focus-visible { outline: 3px solid $color; outline-offset: 2px; }
.lr__label { font-weight: 800; font-size: $label-sm; line-height: 1.1; }
.lr.is-low .lr__label { font-size: $label-lg; }

どこで壊れるか — 落とし穴

この編でいちばん静かに壊れたのは 01 でした。待機画面と注文画面は hidden 属性で切り替えるつもりだったのに、押した後も二つの画面が重なって立ちます。原因は地味なところにあって、display を自分で指定した要素は UA スタイルシートの [hidden] { display: none; } に勝ってしまうからです。.ka [hidden] { display: none; } の一行を足し、この編の中だけは属性のほうが勝つようにしました。

二つ目は 07 で、人が操作した後にしか出てきませんでした。準備中の番号を受け取りの列へ移していくと列が四枠に伸び、320 × 200 の舞台で文書の高さが 239px まで溢れます。渡す前の画面しか測らない自動チェックは黙って通過したので、MAX_READY を 3 に決めて古い枠から捨てる一行で止めました。

三つ目は測りかたの落とし穴です。04 の売り切れ枠を Playwright の page.click() で押そうとすると、aria-disabled="true" の要素を「まだ有効になっていない」と見なして待ち続け、30 秒でタイムアウトして落ちます。押しても何も起きないことを確かめたいのに押せなくて落ちる、という妙な形なので、実測のスクリプトは枠の中心の座標を取って mouse.click() で直に押す書き方に変えました。

四つ目は色です。メニューの価格に使った青 #2f6df6 はクリーム色の背景 #fff7e6 の上では 4.25:1 で AA に届かないので、白いタイル #ffffff の上へ移して 4.53:1 にしました。九項目の文字を全部測って不合格は 0 件で、06 はオレンジの舞台に文字を直接置かず、クリームのカードと墨色の文字にしたので最悪でも 17.11:1 です。段階を枠ではなく線でつなぐ画面が必要なときはフローチャート デザイン 9種がその役を引き受けます。九つの画面をまとめて受け取る zip の鍵は 3abh2hs9 で、開くと項目番号ごとに素の HTML 版と React 版が同じ名前で並んでいます。

事故 出た場所 何が起きたか 直しかた
[hidden] が効かない 01 display 指定が UA の [hidden] に勝ち、二つの画面が重なる .ka [hidden] { display: none; } を一行
舞台からの溢れ 07 受け取りが四枠になり、320 × 200 で文書の高さ 239px MAX_READY を 3 にして古い枠から捨てる
計測が落ちる 04 page.click() が aria-disabled を待ち、30 秒でタイムアウト 枠の中心座標に mouse.click() を直に投げる
文字が沈む 04 青 #2f6df6 はクリーム #fff7e6 の上で 4.25:1 白いタイル #ffffff に移して 4.53:1

アクセシビリティ

九つのプレビューは prefers-reduced-motion: reduce でループを !important で止め、それぞれの画面がいちばん見せたい一枚を残します。01 は一枚目の広告と 34% 進んだバー、02 と 03 は最初の枠を選んだ状態、04 は一枚だけ沈んだタイル、05 は二歩目が開いたシート、06 は開ききったカードと差額 +1,500、07 は二つの番号が各自の場所に立った板、08 は 234deg まで減ったリングと残り 13 秒、09 は低い配置へ降りたまとまりと一段階大きいラベルです。読み上げ側の仕掛けは四つあります。03 のレールはロービングタブインデックス(選ばれた枠だけ tabindex="0"、残りは -1)なので Tab 一回で入って中は矢印で移り、04 の売り切れ枠は disabled ではなく aria-disabled なのでフォーカスが残り、07 の受け取りの列は aria-live="polite" で新しく出た番号を読み、09 の切り替えは aria-pressed で今どちらのモードかを伝えます。押す枠の大きさは WCAG 2.2 のターゲットサイズを下限に置き、動きを止める条件は MDN の prefers-reduced-motion に合わせています。

FAQ

タッチパネル 注文 飲食店 の画面をこのままコピーして使えますか

使えます。九つとも素の HTML と SCSS だけで、外部のライブラリを一つも読み込みません。zip には項目番号ごとに素の HTML 版と React 版が同じ名前で入っているので、メニュー名と価格を自分の店のものに差し替えるところから始められます。ただし在庫と決済の仕組みは入っていないので、そこは手元の POS につなぐ前提です。

画面の大きさは何を基準に決めればいいですか

幅ではなく、指が当たる面積から決めます。このデモは 320 × 200 の舞台で測っていて、02 の言語ボタンは幅 196px、01 のメニュー枠は幅 184px、09 のボタンは clamp(24px, 8vw, 36px) で高さの下限を切らないようにしてあります。画面が小さくなっても押す枠だけは縮ませない、という一点を守れば、あとは余白のほうから削れます。選ぶ枠を横一列で並べたいときはセグメントコントロールUI 9選 — コピペOKの型が近いです。02 のように最初の一枚で人を迎えるカードの骨組みは、ログイン画面 デザイン 9選のカードと同じ問題を解いています。

タッチ専用ですか、キーボードでも動きますか

九つとも、マウス・指・キーボードの三つで同じ結果になることを実測しました。03 は矢印と Home・End、05 のラジオ枠は左右の矢印、04・06・07・09 は Tab で移って Enter か Space で押せます。番号を全部渡し終えて空になった列に何を出すかはエンプティステート デザイン 9選 — CSSだけで描くのほうで扱っています。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。