GODRICH

フローティング ボタン アイコン 9選 — 押す・広がる・動かす

フローティング ボタンとは、画面の隅に浮かんだまま、その画面の代表的な動作をひとつだけ引き受ける丸いボタンのことです。ほとんどは文字を持たず絵ひとつで用件を伝えるため、何をするボタンなのかが支援技術にも届くように、すべてに名前を付けてあります。

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

並べた順は、ボタンが画面の中で背負う仕事の重さの順です。はじめの2つは画面全体の代表動作を握る側で、ひとつ押すと複数の道具に枝分かれし(01)、読んでいる間は自分から席を外します(02)。続く4つは、押されたときの返事の仕方がそれぞれ違います。押された点から波が広がり(03)、星は線を描いてから色を入れ(04)、太陽と月は席を入れ替え(05)、閉じるボタンはリングを回します(06)。最後の3つはボタン自身が形や居場所を変える側で、アイコンが入力欄へ広がり(07)、チャットの板を開いて言葉を積み上げ(08)、長押しで掴めばボタンそのものが引っ越します(09)。

0145度回転して動作を取り出すスピードダイヤル

作成ボタン1つに書く・写真・録音をすべてまとめるなら、押した後で枝分かれする必要があります。この部品はプラスのアイコンを rotate(45deg) で回して閉じる記号に変え、補助ボタン3つを transition-delay で40ミリ秒ずつずらして浮かせます。開閉はクラスだけでなく aria-expanded にも書き込み、Escape でも閉じます。

aria-expandedrotate(45deg)transition-delay
.sd__sub {
  transform: translateY(12px) scale(.6);
  opacity: 0;
  pointer-events: none;
  transition: transform 220ms cubic-bezier(.2, 1.4, .4, 1),
              opacity 140ms linear;
}
.sd.is-open .sd__sub {
  transform: translateY(0) scale(1);
  opacity: 1;
  pointer-events: auto;
}
.sd__item:nth-child(2) .sd__sub { transition-delay: 40ms; }
.sd__item:nth-child(3) .sd__sub { transition-delay: 80ms; }

02スクロール方向を読んで隠れるツールバーFAB

読んでいる最中に道具が本文を覆うなら、それは道具ではなく邪魔です。この部品は一覧の scrollTop が増えれば下へ読み進めていると見なしてボタンの帯を translateY(150%) で舞台の外へ逃がし、値が減れば連れ戻します。2px のしきい値を置いてあるので、指が少し震えた程度では反応しません。

scrollToptranslateYaria-label
var tbLast = tbView.scrollTop;
tbView.addEventListener('scroll', function () {
  var now = tbView.scrollTop;
  var down = now - tbLast > 2;
  var up = tbLast - now > 2;
  tbLast = now;
  if (down && !tb.classList.contains('is-hidden')) {
    tb.classList.add('is-hidden');
  } else if (up && tb.classList.contains('is-hidden')) {
    tb.classList.remove('is-hidden');
  }
});

03押した場所から広がるアイコンのリップル

丸ボタンの真ん中からしか波が出ないと、どこを押したのかという手応えが消えます。この部品は pointerdown が拾った座標を円の --x--y に書き込み、まさにその点から円が scale で膨らんで薄れていきます。膨らませるのは transform なので、レイアウトの計算をやり直しません。

pointerdown--xscale
rpBtn.addEventListener('pointerdown', function (e) {
  var box = rpBtn.getBoundingClientRect();
  var dot = document.createElement('span');
  dot.className = 'rp__wave';
  dot.style.setProperty('--x', (e.clientX - box.left) + 'px');
  dot.style.setProperty('--y', (e.clientY - box.top) + 'px');
  rpBtn.appendChild(dot);
  window.setTimeout(function () { dot.remove(); }, 620);
});

04線を描いてから満たすお気に入りの星

星が一瞬で黄色くなると、お気に入りに入った結果だけが残り、入れる動作そのものは消えます。この部品は輪郭を stroke-dashoffset で100から0まで一周描き、描き終わる460ミリ秒の地点で塗りつぶした星にカットで差し替えます。押した状態は aria-pressed に残り、カードの下地も同じ瞬間に色づきます。

stroke-dashoffsetsteps(1, end)aria-pressed
.st__draw path {
  fill: none;
  stroke: #17141a;
  stroke-width: 10;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset 460ms ease-out;
}
.st__fill { opacity: 0; transition: opacity 0ms 460ms; }
.st.is-on .st__draw path { stroke-dashoffset: 0; }
.st.is-on .st__fill { opacity: 1; }

05太陽と月が入れ替わるテーマボタン

太陽と月を透明度で混ぜると、途中で両方が半分ずつ重なった幽霊のような場面が生まれます。この部品は2つのアイコンを grid-area: 1 / 1 で重ねて置き、transition: opacity 0ms で1コマに切って入れ替え、その後ろのカードと昼夜の文字を steps(1, end) で同じ瞬間に差し替えます。回転と大きさはイージングに任せてあるため、切るのは見え方だけで、回る動きはなめらかに残ります。

steps(1, end)aria-pressedrotate
@keyframes tm-loop-sun {
  0%, 36% { opacity: 1; transform: rotate(0deg) scale(1); }
  52%, 78% { opacity: 0; transform: rotate(90deg) scale(.4); }
  94%, 100% { opacity: 1; transform: rotate(0deg) scale(1); }
}
.tm.is-demo .tm__card {
  animation-name: tm-loop-card;
  animation-timing-function: steps(1, end);
}

06リングが回り続ける閉じるボタン

閉じる記号は小さく、押してよいものか迷わせがちです。この部品はその周りに破線のリングを置き、マウスが乗るか :focus-visible で焦点が届いたときだけリングを回します。キーボードで来た人とマウスで来た人が同じ合図を受け取り、ポインタが外れればリングはすぐに止まり、最初の角度へ戻ります。

:focus-visiblerotatearia-label
.cl__btn:hover .cl__ring,
.cl__btn:focus-visible .cl__ring {
  opacity: 1;
  animation-name: cl-spin;
  animation-duration: 900ms;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes cl-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

07アイコンから入力欄へ広がる検索FAB

虫めがねだけだった場所が入力欄になるには、幅そのものが流れる必要があります。この部品は --w@property で長さ型として登録し、48px から264px まで transition で流します。広がり切ると焦点は preventScroll を添えて入力欄に移り、開いたことは aria-expanded が伝えます。

@propertyaria-expandedpreventScroll
@property --w {
  syntax: "<length>";
  inherits: false;
  initial-value: 48px;
}
.se__pill {
  --w: 48px;
  width: var(--w);
  transition: --w 320ms cubic-bezier(.2, .8, .2, 1),
              border-color 140ms linear;
}

08押すとチャットランチャーが開くヘルプボタン

問い合わせのボタンが別のページへ放り出すと、見ていた画面を失います。この部品は同じ画面の上で板を translateY で持ち上げて開き、送った言葉は実際に吹き出しとして積み上がります。板が開く場所はあらかじめ確保してあるので、開いても下のボタンが押し出されません。

aria-expandedtranslateYaria-live
chFab.addEventListener('click', function () {
  ch.classList.toggle('is-open');
  var open = ch.classList.contains('is-open');
  chFab.setAttribute('aria-expanded', open ? 'true' : 'false');
  if (open) window.setTimeout(function () {
    chInput.focus({ preventScroll: true });
  }, 240);
});

09長押しでドラッグすると追従するFAB

浮いているボタンが、よりによって読みたい文字の上に座ってしまうことがあります。この部品は350ミリ秒より長く押して初めて掴んだ状態に移り、そこからはボタン自身が指を追って translate で動きます。離せば左右の近いほうの端にスナップし、軽いタップは普段どおりのクリックのまま残ります。

pointerdowntranslatearia-label
dgFab.addEventListener('pointerdown', function (e) {
  dgHold = window.setTimeout(function () {
    dgGrab = true;
    dgFab.classList.add('is-grab');
    dgScreen.classList.add('is-holding');
    try { dgFab.setPointerCapture(e.pointerId); } catch (err) { }
  }, 350);
});
dgFab.addEventListener('pointermove', function (e) {
  if (!dgGrab) return;
  var box = dgScreen.getBoundingClientRect();
  var maxX = box.width - dgFab.offsetWidth - 8;
  var x = Math.min(Math.max(e.clientX - box.left - dgFab.offsetWidth / 2, 0), maxX);
  dgFab.style.transform = 'translateX(' + x + 'px)';
});

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

一番よく壊れるのは、文字のないボタンに名前を付け忘れる箇所です。9つのうち6つはアイコン1つきりなので、名前を抜くと読み上げには「ボタン」としか届きません。そこで全部に aria-label を置き、状態が変われば名前も一緒に書き換えます。01のFABは閉じているとき「作成メニューを開く」、開いているとき「作成メニューを閉じる」です。07の検索FABも同じ作法で、05のテーマボタンは今押せば何になるのかを名前に書いてあります。

2番目は、動いたコマ数と変わった面積がまったく一致しないことです。24コマのプレビューで測ると、板ごと開く08は変化の合計面積が27.706%で9つの中で最も大きく、リングだけが回る06は1.828%で最も小さく、その差は15倍ありました。06は24コマ中23コマで何かが動いているのに、動く範囲が細い破線の輪だけなので合計が伸びません。そこでカードの幅を min(252px, 92vw) に絞り、ボタンを56pxまで大きくして、リングが画面で占める取り分を増やしてあります。コマ数だけを見て「よく動いている」と判断すると、この種の部品は代表画像で必ず負けます。

3番目は、消える瞬間のほうが代表画像に選ばれることです。24コマから一番大きく変わったコマを拾う仕組みなので、01のように補助ボタン3つが一斉に引っ込む部品では、開いたコマではなく空の画面が代表になりかねません。今は退場を1コマずつずらし、消えるときに縮小も重ねて、動いたコマが24コマ中9コマに落ち着いています。05はその逆で、steps(1, end) のカットは1コマで終わるため、切り替えだけでは変わるコマがほとんど残りません。カットの前後に円盤が回る区間を広く取り、動いたコマを10コマまで戻しました。

最後は狭い画面です。部品は記事の中では480×300の枠に入り、スマホでは320×200まで縮みます。舞台の余白を引くと縦に使えるのは174pxしかないため、340px 以下では間隔を増やさず減らす方針にしました。01の補助ボタンは最小幅を168pxから140pxに、文字を13pxから11pxに落とし、08のチャット板は高さを148pxから112pxに縮めます。09は画面が狭くなるとボタンの大きさも変わるので、スナップの着地点を固定値で書かず、画面の幅からボタンの幅を引いてそのつど計算します。縮めたあとに9つとも測り直し、320×200の枠から中身がはみ出す部品が1つも残っていないことを確かめました。狭い画面で崩れる部品は、広い画面でどれだけ気持ちよく動いても使えません。9つ分を1つのフォルダーにまとめてあり、その圧縮ファイルのパスワードは khf267ta です。中には今見ている素の版と、同じ動きをするReact版が並んで入っています。

アクセシビリティ

動きを減らす設定(prefers-reduced-motion: reduce)が入った画面では、9つとも繰り返しのループが止まり、結果の状態だけが残ります。スピードダイヤルは開いたまま、星は描き終わって塗られたまま、テーマボタンは月が出たまま、チャットは返事まで届いた画面のまま立ち止まります。09は掴んだ瞬間の大きさで左端に、02の道具は隠れずに見えている状態で止まります。動きが消えても「今はどの状態なのか」だけは消してはならないからです。

アイコンボタンのアクセシビリティは、名前と状態の2つに整理できます。名前は aria-label で与え、開け閉めは aria-expanded(01・07・08)、オンとオフは aria-pressed(04・05)で伝えます。焦点の扱いも省いていません。07と08が入力欄に焦点を移すときに preventScroll を併用するのは、これらの部品が一覧画面の iframe の中に置かれていて、そのまま呼ぶと親のページごと引きずられるからです。06はマウスが乗ったときとキーボードの焦点が届いたときに同じ合図を出すよう、:focus-visible をホバーと並べて束ねてあります。

項目 見た目 スクリーンリーダー 操作
01 スピードダイヤル 45度の回転と3方向の展開 aria-expanded の切替 クリック · Escape
02 ツールバーFAB 下へ退く帯 ボタンごとの aria-label スクロール
03 アイコンのリップル 押した点から広がる波 お知らせボタンの名前 ポインター · Enter
04 お気に入りの星 線を描いてから塗り aria-pressed · 保存の案内 クリック
05 テーマボタン 太陽と月の入れ替え aria-pressed · 名前の書き換え クリック
06 閉じるリング 回り始めるリング 閉じるボタンの名前 ホバー · Tab の焦点
07 検索FAB アイコンから欄へ aria-expanded · 欄の名前 クリック · Escape
08 ヘルプFAB 持ち上がる板 aria-live による返事の案内 クリック · Enter
09 ドラッグFAB 掴んだ先へのスナップ 位置の案内文 長押しドラッグ · ←→

アイコンだけのボタンに名前を与える方法は、MDN の aria-label の解説にまとまっています。同じ系統のナビゲーション部品はナビのカテゴリに、JavaScript が実際に走る部品はJS のカテゴリに集めてあります。

FAQ

アイコンだけのボタンに文字は要りますか?

画面に見える文字はなくてかまいませんが、名前は必ず要ります。aria-label で与えれば目には映らず、読み上げにだけ届きます。02の3つのボタンのように横並びのアイコンが続く場面は特にそうで、見る人は矢印・虫めがね・三本線を見分けられても、聞く人には「ボタン ボタン ボタン」としか届きません。名前は「一番上へ」のように、押すと何が起きるかの言葉で書きます。

アイコンの色はどれくらい濃ければよいですか?

アイコンは文字ではなく絵なので、判断の基準が違います。意味を運ぶ絵は背景との差が3対1あれば足り、本文の文字に求められる4.5対1はここには当てはまりません。この記事では、濃い墨色の面にクリーム色のアイコンを置くか、白いカードの上に墨色のアイコンを置くかの2通りに寄せてあります。逆に02の黄色いアイコンのように明るい色を使う場合は、墨色の帯に乗せて初めて絵として読めるようになります。

浮いているボタンは画面のどこに置くのがよいですか?

右手の親指が届く右下が基本ですが、決まりではありません。左利きの人や画面の大きい端末ではその場所がかえって遠く、下部にタブバーがあれば重なります。09を作った理由がそこにあります。置き場所をこちらで決める代わりに長押しで動かせるようにし、指を離せば近いほうの端に吸い付きます。動かせない作りにするなら、せめてタブバーの高さぶんは浮かせておきます。

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

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