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

タイピングインジケーターUI 9選 — 3つの点からグループチャットまで

「タイピングインジケーター」9選 — 9つのうち1つは入力中の時間が長いほどアバターの縁がゆっくり脈打ち、1つはグループチャットで入力者が変わるたびに残り時間の値で名札の幅を描き直し、もう1つは振幅配列の値で音声入力表示の棒の高さを計算し直します。

実際のメッセンジャーアプリのUI・ブランド・実名はどこにもなく、画面内の送信者・名前はすべて架空のものです。9項目のうち08(表示オフトグル)だけに実際のクリックリスナーがあり、残り8項目はタイマーまたはrequestAnimationFrameの固定周期による自動再生だけで実演します。すべての項目のデータ(間隔・周期・振幅配列など)は固定値のため、再生のたびに同じ場面が再現されます。

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

デモ

9項目とも、点の順番・経過時間・参加者数・時間差・残り時間・振幅配列・状態値・キーボード高さという値を毎フレームまたは一定周期で求め直し、遅延・幅・周期・ラベル・棒の高さ・透明度・位置という画面上の変化に変換するという同じ前提を共有しています。実測値は次のとおりです。

番号 項目 累積面積 強度平均 動いたフレーム
01 3点バウンス 0.23% 60.4 15/23
02 吹き出しモーフィング 4.59% 191.1 13/23
03 複数ユーザー重なり 0.34% 105.7 6/23
04 アバターパルス同期 2.16% 55.9 19/23
05 既読マージ 3.03% 78.3 9/23
06 グループチャット循環 0.33% 156.8 23/23
07 音声入力波形 1.03% 187.9 23/23
08 表示オフトグル 6.37% 80.5 11/23
09 キーボードバーヒント 11.55% 106.9 12/23

累積面積は09キーボードバーヒントが11.55%でもっとも広く、08表示オフトグル6.37%・02吹き出しモーフィング4.59%・05既読マージ3.03%・04アバターパルス同期2.16%・07音声入力波形1.03%・03複数ユーザー重なり0.34%・06グループチャット循環0.33%・01点3つバウンス0.23%の順です。01・06・03の面積が9項目中とくに小さいのは、点・ラベル・名札のように画面上で実際に占めるピクセルの場所自体が小さい要素だからで、欠陥ではありません。24フレームキャプチャ(デルタ23個)のうち動いたフレームは6(03)〜23(06・07)の間にあり、強度平均は55.9(04、もっとも低い)〜191.1(02、もっとも高い)の間で、9項目とも画面が動き続ける下限値に十分な余裕を持って収まっています。

タイピングインジケーターの解剖 — 9つの画面が計算するもの

そもそもタイピングインジケーターとは何を指すのか、この9つの画面はその答えを1つの見た目ではなく9通りの計算として示します。点が跳ねる順番・点と吹き出しの幅の差・同時入力者数・入力が続いた時間・2つの時刻の差・残り表示時間の比率・振幅配列の値・状態値・キーボードの高さという9種類の値が、毎フレームあるいは状態が切り替わった瞬間に読み直され、遅延・幅・周期・ラベル・棒の高さ・透明度・位置という画面上の変化に変換されています。

入力と出力 — 何を計算して何に変えるか

入力は点の順番の値(index)、点3つの合計幅と吹き出し1つの幅、getBoundingClientRectで測った「他N人」ラベルの実際の幅、入力が継続した時間、入力表示が消えた時刻とメッセージが届いた時刻の差、残り表示時間の比率、あらかじめ決めた振幅値の配列、クリックで切り替わる状態値(オン・オフ)、仮想キーボードの高さ値という9種類です。出力はCSSカスタムプロパティ(--delay・--w・--bw・--period)、transformのscaleX・scaleY・translateY、style.opacity、classListの切り替え、textContentです。01は--delayの値をCSSの@keyframes dotBounceへ一度渡すだけで以降JavaScriptは関与せず、04も--periodの値を@keyframes ringPulseへ渡し続けるだけで、跳ねる・脈打つ動き自体はキーフレームが担い、JavaScriptは周期や遅延の値だけを求め直しています。

01の自動再生と08だけの実際のクリックリスナー

9項目のうち01・02・03・04・05・06・07・09の8項目はタイマーまたはrequestAnimationFrameの固定周期による自動再生だけで実演されており、実際のクリック・ドラッグ・ポインターのリスナーは一つも付いていません。08(表示オフトグル)だけに実際のクリックリスナーが付いていて、押された瞬間の状態値を基準に同じ補間が最初から再生し直されます。「入力中 アニメーション」を探す人の多くはこの自動再生の8項目のような固定周期の演出を求めています。

実際のメッセンジャーAPIを使わない理由

このデモはどの項目も実際のWebSocket接続・サーバー通信・実キーを一切使いません。入力中・既読・音消しのような状態はタイマーと名前付きの定数だけで進み、実サービスに組み込む場合はこの状態遷移が起きる場所に実際のリアルタイムイベントの信号を差し込む必要があります。画面内の送信者名・アイコンはすべて架空のもので、特定のメッセンジャーアプリの名前・ロゴ・ブランドはどこにもありません。

画面別コード

「タイピングインジケーター」を探す人の多くは点3つが跳ねる基本形だけを思い浮かべますが、この9項目は吹き出しへの合流・複数人の重なり・既読との併存・グループの循環・音声波形・音消し・キーボードとの位置関係という異なる場面ごとに、実測すべき値も変わることを示しています。似た計算の考え方は、ボットの応答待ちを扱うAIチャットローディングアニメーションcss9選の再生ロジックとも軸が異なりますが、固定値だけで場面を再現する姿勢は共通しています。

013点バウンス — インデックスで順番を計算する基本形

入力中の状態になると点が3つ現れます。それぞれの点が跳ね始める時点は、点の順番の値(index)にDOT_STAGGER_MS120msという間隔を掛けて求め直し、--delayというCSSカスタムプロパティとして渡されます。

3点バウンス — インデックスで順番を計算する基本形
新しいタブで開く

01 · 3点バウンス — インデックスで順番を計算する基本形 — 計測 累積面積 0.225% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

animation-delaysetPropertyaria-live

indexに間隔を掛けてanimation-delayを求め直す基本形

dot.dataset.iから読んだ順番の値にDOT_STAGGER_MSを掛けるだけなので、点の数が2個に減っても4個に増えても間隔の計算式は変わりません。点の順番を掛けずに遅延値を固定で並べると、点の数が変わったときに間隔の計算が崩れます。

(function () {
  var DOT_STAGGER_MS = 120;
  var dots = document.querySelectorAll('.dot');
  if (!dots.length) return;
  dots.forEach(function (dot) {
    var i = Number(dot.dataset.i || 0);
    dot.style.setProperty('--delay', (i * DOT_STAGGER_MS) + 'ms');
  });
})();

02吹き出しモーフィング — 3つの点が1つの吹き出しの大きさに合わさる転換

入力中の表示が終わった状態になると、点3つの合計幅(DOT_W56px)の値が吹き出し1つの幅(MSG_W168px)の値へeaseOutカーブで補間され、補間が終わる時点に合わせてメッセージ内容が場所を引き継ぎます。実際のメッセンジャーアプリの会話画面を想定していますが、画面内の送信者・文面はすべて架空のものです。

吹き出しモーフィング — 3つの点が1つの吹き出しの大きさに合わさる転換
新しいタブで開く

02 · 吹き出しモーフィング — 3つの点が1つの吹き出しの大きさに合わさる転換 — 計測 累積面積 4.594% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setPropertyrequestAnimationFrameprefers-reduced-motion

経過比率をeaseOutで曲げてから幅の値を求め直す

CYCLE_MS2000msの周期のうちMORPH_START200msの位置で補間が始まり、MORPH_MS640msの間で--wが56から168へ進み、REVERT_AT1240msの位置で最初の状態へ戻ります。点や吹き出しの幅を求めずにdisplay:noneなどで即座に切り替えると、モーフィングの存在意義が消えてしまいます。

  var DOT_W = 56;
  var MSG_W = 168;
  var CYCLE_MS = 2000;
  var MORPH_START = 0.10 * CYCLE_MS;
  var MORPH_MS = 640;
  var REVERT_AT = 0.62 * CYCLE_MS;

  function easeOut(p) { return 1 - Math.pow(1 - p, 3); }

  function apply(t) {
    var inMorph = t >= MORPH_START && t < REVERT_AT;
    var p = Math.min(Math.max((t - MORPH_START) / MORPH_MS, 0), 1);
    var eased = easeOut(p);
    var w = inMorph ? DOT_W + (MSG_W - DOT_W) * eased : DOT_W;
    bubble.style.setProperty('--w', w.toFixed(1));
    var done = inMorph && p >= 1;
    dots.style.opacity = done ? '0' : '1';
    msg.style.opacity = done ? '1' : '0';
  }

03複数ユーザー重なり — 同時入力者数を名札の幅で計算する行

同時に入力中の人が2人以上の状態になると、名前を並べる代わりに「+N」ラベルのテキストをmeasure要素に流し込み、getBoundingClientRectで実際の描画幅を人数ごとに実測して--bwへ反映します。

複数ユーザー重なり — 同時入力者数を名札の幅で計算する行
新しいタブで開く

03 · 複数ユーザー重なり — 同時入力者数を名札の幅で計算する行 — 計測 累積面積 0.337% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

getBoundingClientRectsetPropertyrequestAnimationFrame

人数ごとに実測した幅の値を補間して描き直す

COUNTS[2,3,4]をHOLD_MS620msごとに巡り、MORPH_MS300msの間だけ次の人数の幅へ補間します。同時入力者数を数えずに名前をすべて並べると、人数が増えたときに行があふれたり文字が切れたりします。

  function widthFor(n) {
    measure.textContent = '+' + n;
    return measure.getBoundingClientRect().width;
  }

  var widths = COUNTS.map(widthFor);
  var CYCLE_MS = HOLD_MS * COUNTS.length;

  function apply(t) {
    var phase = Math.floor(t / HOLD_MS) % COUNTS.length;
    var next = (phase + 1) % COUNTS.length;
    var phaseT = t % HOLD_MS;
    var p = Math.min(phaseT / MORPH_MS, 1);
    var w = widths[phase] + (widths[next] - widths[phase]) * p;
    badge.style.setProperty('--bw', w.toFixed(1));
    badge.textContent = '+' + COUNTS[phase];
  }

04アバターパルス同期 — 入力継続時間で鼓動周期を求め直す円

入力中の時間が長くなるほど、アバターの縁の鼓動周期(--period)が経過時間の値に比例して徐々に遅くなり、入力が止まった状態になると周期がBASE_PERIOD_MS900msという元の値に戻ります。

アバターパルス同期 — 入力継続時間で鼓動周期を求め直す円
新しいタブで開く

04 · アバターパルス同期 — 入力継続時間で鼓動周期を求め直す円 — 計測 累積面積 2.16% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setPropertyclassList.togglesetInterval

tickの値にPULSE_SLOWDOWN_MS_PER_SECを掛けて周期を求め直す

TICK_MS150msごとにtickを1ずつ進め、period = BASE_PERIOD_MS + tick * PULSE_SLOWDOWN_MS_PER_SEC(40ms)で周期を求め直し、MAX_TICKS8で一周してから0に戻ります。入力継続時間を測らずに周期を固定値のままにすると、「長く入力中」という情報が画面から消えてしまいます。

  var BASE_PERIOD_MS = 900;
  var PULSE_SLOWDOWN_MS_PER_SEC = 40;
  var TICK_MS = 150;
  var MAX_TICKS = 8;

  var tick = 0;
  ring.style.setProperty('--period', BASE_PERIOD_MS + 'ms');

  function step() {
    tick = (tick + 1) % (MAX_TICKS + 1);
    var typing = tick > 0;
    var period = BASE_PERIOD_MS + tick * PULSE_SLOWDOWN_MS_PER_SEC;
    ring.style.setProperty('--period', period + 'ms');
    ring.classList.toggle('is-typing', typing);
    status.textContent = typing ? '入力中' : '待機';
  }

05既読マージ — 消えた時間差から合流の瞬間を求めるラベル

入力表示が消えた時点とメッセージが届いた時点の時間差(gap)をDate.now()の差で求め、その値がMERGE_GAP_MS400msという基準より小さい状態になると、入力表示と既読ラベルを1つの場所にまとめます。

既読マージ — 消えた時間差から合流の瞬間を求めるラベル
新しいタブで開く

05 · 既読マージ — 消えた時間差から合流の瞬間を求めるラベル — 計測 累積面積 3.026% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setTimeoutclassList.togglearia-live

2つの時刻の差を基準値と比較して合流を判定する

入力表示が消えてからARRIVE_DELAY_MS180ms後にメッセージが届く設定のためgapはつねに180前後になりますが、値そのものは毎周期Date.now()から求め直しています。時間差を測らずに表示をつねに別々にする、またはつねにまとめると、合流判定という計算の意味が失われます。

  function runCycle() {
    dots.style.opacity = '1';
    msgRow.style.opacity = '0';
    receipt.classList.remove('is-merged');
    timer1 = setTimeout(function () {
      dots.style.opacity = '0';
      var vanishAt = Date.now();
      timer2 = setTimeout(function () {
        var arrivedAt = Date.now();
        var gap = arrivedAt - vanishAt;
        var merged = gap < MERGE_GAP_MS;
        msgRow.style.opacity = '1';
        receipt.classList.toggle('is-merged', merged);
        live.textContent = merged ? '統合表示' : '個別表示';
      }, ARRIVE_DELAY_MS);
    }, VANISH_AT);
  }

06グループチャット循環 — 入力者が変わるたびに残り時間で幅を描き直すラベル

グループチャットで入力中の人がNAMES(A・B・C)の間で切り替わるたびに、前の人の名前が残る時間の比率(remain)を毎フレーム求め直し、fillのscaleXへ反映してから次の人の名前へ切り替えます。

グループチャット循環 — 入力者が変わるたびに残り時間で幅を描き直すラベル
新しいタブで開く

06 · グループチャット循環 — 入力者が変わるたびに残り時間で幅を描き直すラベル — 計測 累積面積 0.329% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

requestAnimationFrametransform-originscaleX(

remainの値をscaleXへ反映して次の人へ切り替える

HOLD_MS640msごとにNAMESを巡回し、phaseTが0から640msへ進む間にremainが1から0へ減っていきます。残り表示時間を測らずにラベル幅を固定値のままにすると、次の人に切り替わるタイミングが不自然になります。

  var NAMES = ['A', 'B', 'C'];
  var HOLD_MS = 640;
  var CYCLE_MS = HOLD_MS * NAMES.length;

  function apply(t) {
    var idx = Math.floor(t / HOLD_MS) % NAMES.length;
    var phaseT = t % HOLD_MS;
    var remain = 1 - phaseT / HOLD_MS;
    who.textContent = NAMES[idx];
    fill.style.transform = 'scaleX(' + remain.toFixed(3) + ')';
  }

07音声入力波形 — 振幅配列の値で棒の高さを描き直す表示

音声メッセージを録音中の状態になると、あらかじめ決めた16個の振幅値の配列(AMPLITUDES)をAMPLITUDE_TICK_MS80msごとに順に読み、棒ごとの高さ(scaleY)を求め直します。

音声入力波形 — 振幅配列の値で棒の高さを描き直す表示
新しいタブで開く

07 · 音声入力波形 — 振幅配列の値で棒の高さを描き直す表示 — 計測 累積面積 1.029% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setIntervaltransform-originscaleY(

配列のインデックスをずらして棒ごとの高さを求め直す

idxを1ずつ進めながら(idx + i) % AMPLITUDES.lengthで棒ごとに違う位置の振幅値を読むため、12本の棒が同じ配列から少しずつずれた高さを表示します。振幅配列を順に読まずに棒の高さをランダムや固定値のままにすると、波形らしく見えず再現性も失われます。

  var AMPLITUDE_TICK_MS = 80;
  var AMPLITUDES = [.25, .55, .82, .38, .68, .92, .30, .60, .78, .42, .58, .88, .34, .64, .48, .70];

  var idx = 0;
  function tick() {
    bars.forEach(function (bar, i) {
      var v = AMPLITUDES[(idx + i) % AMPLITUDES.length];
      bar.style.transform = 'scaleY(' + v.toFixed(2) + ')';
    });
    idx = (idx + 1) % AMPLITUDES.length;
  }

08表示オフトグル — 状態値だけ透明度を補間するスイッチ

表示がオフの状態になると、入力表示カードの透明度がOFF_OPACITY0.3という値まで補間されて薄くなり、aria-pressedと読み上げ用のラベルが同時に切り替わります。「タイピングインジケーター オフ」を探す人が求めているのはこの状態値と透明度の対応です。

表示オフトグル — 状態値だけ透明度を補間するスイッチ
新しいタブで開く

08 · 表示オフトグル — 状態値だけ透明度を補間するスイッチ — 計測 累積面積 6.372% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

addEventListeneraria-pressedopacity

クリックされた状態値を基準にopacityを補間する

btnに付いた実際のクリックリスナーがmutedを反転させ、apply()がcard.style.opacityとbadge.hiddenとaria-pressedをまとめて更新します。状態値を補間せずに透明度を即座に0と1へ切り替えると、オン・オフの切り替えがぶつっと途切れて見えます。

  var OFF_OPACITY = 0.3;
  var AUTO_CYCLE_MS = 700;
  var muted = false;

  function apply() {
    card.style.opacity = muted ? String(OFF_OPACITY) : '1';
    badge.hidden = !muted;
    btn.setAttribute('aria-pressed', String(muted));
    live.textContent = muted ? '表示オフ' : '表示オン';
  }

  btn.addEventListener('click', function () {
    muted = !muted;
    apply();
  });

09キーボードバーヒント — 仮想キーボードの高さ値で位置を求め直す表示

モバイル画面で仮想キーボードが開いた状態になると、キーボードが占める高さの値(heightAt)をイージングで毎フレーム求め直し、その値だけ入力表示の下側の位置(translateY)を求め直してキーボードバーのすぐ上に付けます。

キーボードバーヒント — 仮想キーボードの高さ値で位置を求め直す表示
新しいタブで開く

09 · キーボードバーヒント — 仮想キーボードの高さ値で位置を求め直す表示 — 計測 累積面積 11.546% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateY(requestAnimationFrametransform

heightAtの値をtranslateYへ反映して位置を求め直す

KB_H64pxをCYCLE_MS2000msの周期の中でOPEN_MS360msかけて開き、CLOSE_MS360msかけて閉じ、開いている間はkeyboardとindicatorの両方が同じ高さの値だけ動きます。キーボードの高さを計算せずに入力表示の位置を固定値のままにすると、キーボードが開いても表示が隠れたり浮いたりします。

  function heightAt(t) {
    if (t < OPEN_START) return 0;
    if (t < OPEN_START + OPEN_MS) {
      return KB_H * ease((t - OPEN_START) / OPEN_MS);
    }
    if (t < CLOSE_START) return KB_H;
    if (t < CLOSE_START + CLOSE_MS) {
      return KB_H * (1 - ease((t - CLOSE_START) / CLOSE_MS));
    }
    return 0;
  }

  function apply(h) {
    keyboard.style.transform = 'translateY(' + (KB_H - h) + 'px)';
    indicator.style.transform = 'translateY(' + (-h) + 'px)';
  }

数値が画面を変える場所 — 経過時間・参加者数・振幅

9項目の計算層を経過時間・参加者数・振幅という3つの軸でまとめ直すと、同じ「値を求め直す」という動作が違う入力に対して繰り返されていることが分かります。

経過時間が周期・時間差・位置に変わる場所

04では経過時間(tick)が鼓動周期に、05では2つの時刻の差が合流判定に、09では周期内の経過時間がイージングを通じてキーボードの高さに変わります。同じ「時間を測る」という動作でも、比例させるか閾値と比べるかイージングにかけるかで画面上の変化はまったく違う形になります。

参加者数が名札の幅とラベルの切り替えに変わる場所

03では同時入力者数が「+N」ラベルの実測幅に、06では入力者の切り替わりが残り時間の比率とラベルのscaleXに変わります。人数や順番という整数の値をそのままテキストにせず、実測した幅や比率という連続的な値に変換している点が共通しています。

振幅配列と状態値が棒の高さと透明度に変わる場所

07では固定の振幅配列が棒ごとのscaleYに、08ではクリックで切り替わる状態値がopacityの補間先に変わります。どちらも配列や真偽値という離散的な値を、requestAnimationFrameまたはtransitionを通じて連続的な見た目の変化へつなぎ直しています。

どこで壊れるか — 複数ユーザーの重なり・再接続・低スペック端末

ここまでの9項目は固定値の再生を前提にしていますが、実サービスに組み込む際に値が想定より大きくなったり順序が乱れたりする場面を考えておく必要があります。進捗の割合を幅に変える計算という点では、アップロードプログレスバーローダー9種が扱う再開・失敗時の幅の巻き戻しとも近い注意点です。

複数ユーザーが同時に入力するとどうなるか

03のwidthForはCOUNTS[2,3,4]という固定の人数しか実測していないため、実サービスで人数が5人・10人と増えた場合は同じ関数をその都度呼び直して幅を実測し直す必要があります。実測せずに人数だけをテキストに反映すると、ラベルの幅が足りずに文字が切れる場合があります。

再接続でタイマーが重なるとどうなるか

05はsetIntervalとsetTimeoutを組み合わせて1つの周期を作っているため、実サービスで接続が切れて再接続した瞬間にタイマーを止めずに新しいタイマーを重ねると、vanishAtとarrivedAtの対応がずれてgapの値が実際の時間差と合わなくなります。

低スペック端末で振幅更新が追いつかないとどうなるか

07はAMPLITUDE_TICK_MS80msごとにscaleYを書き換えていますが、低スペック端末でこの間隔のまま描画が追いつかないと棒の動きが飛び飛びに見えることがあります。バリエーションの節にある220ms版のように間隔を広げるのは、この状況に対する実際の対処の1つです。

3つのバリエーションとアクセシビリティ

同じ計算式でも定数の値を変えるだけで印象が変わることを、3つの項目で確かめられます。9種のタイピングインジケーターUIはすべてyb2bq6qtで開くzipにまとめられており、vanillaフォルダには9項目それぞれのindex.htmlが、reactフォルダには同じ計算をコンポーネント化したtsxファイルが入っています。

点の遅延間隔 — 120msと260ms

01のDOT_STAGGER_MSを120msのままにすると3つの点がほぼ同時に跳ねてバウンスがひとまとまりに見え、260msへ延ばすと点の間隔が広がって波がゆったり流れる代わりに全体の周期が伸びて間延びした印象になり得ます。このzipには120ms版が入っています。

アバターパルス減速係数 — 40msと120ms

04のPULSE_SLOWDOWN_MS_PER_SECを40msのままにすると長く入力しても鼓動がほとんど遅くならず、120msへ上げると数秒で鼓動が目立って伸びて「長く入力中」という印象がはっきりします。このzipには40ms版が入っています。

振幅更新周期 — 80msと220ms

07のAMPLITUDE_TICK_MSを80msのままにすると棒が滑らかにつながって実際の波形のように見え、220msへ延ばすと棒が途切れて動くため低スペック端末の見え方に近づきます。このzipには80ms版が入っています。

prefers-reduced-motionをどう扱うか

matchMedia('(prefers-reduced-motion: reduce)')で検知した場合、9項目とも繰り返し再生(自動ループ)だけを止め、その時点までの計算結果(遅延値・補間幅・鼓動周期・合流判定・ラベル幅・棒の高さ・透明度・位置)はそのまま見せています。計算そのものを消してしまうと、利用者が減速を求めた理由と違う体験になってしまいます。

role=statusとaria-pressedをどう更新するか

入力中・既読のような状態文言にはrole="status"とaria-live="polite"を付けてスクリーンリーダーに知らせており、この設計は01から07までの7項目に共通しています。08のトグルボタンはbutton要素とaria-pressed属性で押された状態をそのまま伝え、09はキーボードの高さに合わせて表示位置を動かすだけで、読み上げ用の案内文は別途置いていません。

FAQ

実際のメッセンジャーアプリのAPIやWebSocketを使っていますか

いいえ。9項目のどれも実際のWebSocket接続・サーバー通信・実キーを呼び出していません。入力中・既読・音消しの状態はすべてタイマーと名前付きの定数によるシミュレーションです。

タイピングインジケーター インスタ のようにアプリ名で検索しても使えますか

「タイピングインジケーター インスタ」のように特定のアプリ名を添えて探す人もいますが、この記事のデモはどのアプリの実装仕様書でもなく、パターンとしての計算方法を示すものです。画面内の送信者名・アイコンはすべて架空のもので、特定のブランドのUIを再現したものではありません。

08の音消しトグル以外にクリックで操作できる項目はありますか

ありません。01・02・03・04・05・06・07・09はクリック・ドラッグ・ポインターのリスナーを一つも持たず、タイマーまたはrequestAnimationFrameの固定周期だけで自動再生されます。実際のユーザー操作を前提とした状態遷移は08だけです。

もっと読む

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

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