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

ティッカー ui で作るリアルタイム株価表示9種 — APIキー不要

ティッカー ui とは、株価のように絶えず変わる数字を、いまどこが変わったのかが目で分かる形にして見せる画面部品のことです。ここに並べた九つは、証券会社のキーも有料のデータ購読もいらず、ファイルを貼るだけでそのまま動きます。

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

九つの並びは人気順ではなく、ひとつの値が届いてから人の判断に至るまでの道順です。はじめの三つは値そのものを見せる側で、変わった行が光り(01)、横に流れる帯が一覧を圧縮し(02)、直近の推移を細い線が添えます(03)。次の三つは値を形に積む側で、未確定のローソクが伸び(04)、板の厚みが伸び縮みし(05)、銘柄全体が色の地図になります(06)。最後の三つは判断を助ける側で、評価損益の桁が回り(07)、決めた線を超えたら知らせ(08)、そもそも今つながっているのかを示します(09)。株価 ティッカー と検索して最初に出てくるのは02のような横帯ですが、この編が扱うのは帯の形ではなく「値が変わった」という出来事の伝え方です。データを一度だけ描く側はcss グラフ をSVGで描く9種に、描き終えた図を触って読む側はダッシュボード グラフ デザイン 操作9種にあり、こちらは描いたあとも値が入り続ける側です。価格は mulberry32 という短い擬似乱数の種を固定して作っているので、誰が開いても同じ流れが再現され、銘柄名も実在しないものだけを使っています。秒ではなく一日単位で値を積み上げる側は、同じdashboard柱の習慣トラッカー UI 9選が受け持っており、更新の間隔だけが違う同じ問題を反対側の端から解いています。

01上昇は緑・下落は赤に光る株価セル

何が見えるか

新しい値が届いた行だけ、背景が一フレームで点いて、ゆっくり抜けます。数字は前の値と今の値を同じマスに重ねて置き、入れ替えは中間のないカットなので、二つの値が半透明で重なる瞬間がありません。

上昇は緑・下落は赤に光る株価セル
新しいタブで開く

01 · 上昇は緑・下落は赤に光る株価セル — 計測 累積面積 19.33% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

steps(1, end)tabular-numssetInterval

コードのどこを見るか

2秒のプレビューで光るのは三行、毎秒1.5回で、累積面積19.33%・動いたフレーム9/23を記録しました — 実使用では900msごとに一行ずつ値が変わるので、ウォッチリストの行やリアルタイム約定画面の現在値の列にそのまま置けます。

// 前の値と今の値を同じマスに重ねる — カット差し替えなので幅が変わっても位置がぶれない
.pf__priceWrap, .pf__chgWrap { display: grid; justify-items: end; }
.pf__price, .pf__chg { grid-area: 1 / 1; font-variant-numeric: tabular-nums; }
.pf__price {
  font-weight: 800; font-size: clamp(12px, 3.4vw, 15px); line-height: 1.5; color: $subject-cream;
}
.pf__chg {
  font-weight: 700; font-size: clamp(10px, 2.6vw, 11px); line-height: 1.5; min-width: 62px;
}
.pf__chg.is-up { color: $tick-up; }
.pf__chg.is-down { color: $tick-down; }
// 実使用(静的な経路)では今の値だけが見える — 前の値は場所だけ保って見えない
.pf:not(.is-demo) .pf__price--prev, .pf:not(.is-demo) .pf__chg--prev { opacity: 0; }

// 実使用のフラッシュ — JS が付けた is-up / is-down が一フレームで背景を点け、自分で消える。
// steps(1, end) で切り替わるのは値のほうで、背景は点いた状態から続けて抜ける
.pf__row.is-up { animation: pfFlashUp 600ms $easing 1; }
.pf__row.is-down { animation: pfFlashDown 600ms $easing 1; }
@keyframes pfFlashUp { from { background-color: $flash-up; } to { background-color: transparent; } }
@keyframes pfFlashDown { from { background-color: $flash-down; } to { background-color: transparent; } }

02横に流れるティッカーテープ

どんな動きか

同じ並びを二組つなげた帯が右から左へ等速で流れ、ポインタを載せるかキーボードのフォーカスが入ると、その場でぴたりと止まります。止めているのは animation-play-state ひとつだけなので、JS で位置を捕まえておく必要がありません。

横に流れるティッカーテープ
新しいタブで開く

02 · 横に流れるティッカーテープ — 計測 累積面積 2.469% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateXanimation-play-statemargin-right

どこを変えるか

動いたフレームは23/23で、使っているアニメーションはこの一つきり、累積面積は2.469%です — 実使用は毎秒45pxほどの速さで、サービス上部の固定株価バーや大型ロビーモニターに向きます。

.tt__track {
  display: flex; width: max-content;
  animation-name: ttSlide;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
}
.tt__half { display: flex; }
.tt__item {
  display: inline-flex; align-items: center; flex-shrink: 0; white-space: nowrap;
  margin-right: $sp-6;   // トラックの gap の代わり — 一組の幅が (マス + 間隔) × N でぴったり割り切れる
}

03新しい値が入るたび左へずれるスパークライン

画面で起きること

新しい値が来るたび、線は右端へ一点ぶん伸びて左へ一目盛りずれます。60点を毎回引き直していないので、三行が同時に流れても計算はほとんど増えません。

新しい値が入るたび左へずれるスパークライン
新しいタブで開く

03 · 新しい値が入るたび左へずれるスパークライン — 計測 累積面積 10.465% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

polylinetranslateXpreserveAspectRatio

コードの読み方

動いたフレーム17/23・累積面積10.465%で、ティッカー css というと横に流れる帯を思い浮かべますが、こちらは監視テーブルの行に細い線を添える使い方に向きます。

// 新しい値ひとつ = 点ひとつ + 一目盛り。60点を引き直さないことがこの部品のすべて
function tick(r) {
  var mid = (r.lo + r.hi) / 2, span = r.hi - r.lo;
  // 平均へ少しずつ戻るランダムウォーク — 目盛りの外へ永久に逃げていかない
  var p = Math.round(r.price + (rand() - 0.5) * span * 0.22 - (r.price - mid) * 0.05);
  r.price = p;
  r.line.setAttribute('points', r.line.getAttribute('points') + ' ' + r.count + ',' + toY(r, p).toFixed(2));
  r.count += 1;
  r.shift += 1;
  r.track.style.transform = 'translateX(-' + r.shift + 'px)';
  var chg = (p - r.base) / r.base * 100;
  r.num.textContent = money(p);
  // 方向を色だけで伝えない — 矢印と符号をいつも一緒に書く
  r.chg.textContent = arrow(chg) + ' ' + pct(chg);
  r.chg.className = 'sp__chg ' + (chg >= 0 ? 'is-up' : 'is-down');
  if (r.count > 400) { rebase(r); }
}

04最後のローソクが伸びるキャンドルチャート

何が見えるか

確定した11本はそのまま立っていて、右端の一本だけが新しい値のたびに伸び縮みします。上げ下げを色だけで分けず、上げは中空の胴、下げは塗りつぶした胴と形まで変えてあります。

最後のローソクが伸びるキャンドルチャート
新しいタブで開く

04 · 最後のローソクが伸びるキャンドルチャート — 計測 累積面積 2.771% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleYtransform-originsteps(1, end)

コードのどこを見るか

動いたフレームは23/23、累積面積は2.771%と小さいのですが、その一本が常に動き続けるのがこの部品で、チャート画面の分足エリアや期間切り替えタブの下に置きます。

.cl__wick { position: absolute; left: calc(50% - 1px); width: 2px; background: $tick-mute; }
// ヒゲも胴も **始値の側の辺** が基準点。中央を基準に伸ばすと、すでに確定した始値の位置まで
// 一緒に動いてチャートが嘘をつく — 上へ伸びるものは下の辺、下へ伸びるものは上の辺が origin
.cl__wick--up { transform-origin: 50% 100%; }
.cl__wick--dn { transform-origin: 50% 0; }
.cl__slot--live .cl__wick, .cl__slot--next .cl__wick { background: $color; }

.cl__body { box-sizing: border-box; position: absolute; left: 18%; right: 18%; border-radius: $r-xs; }
// 方向を色だけで伝えない — 上げの足は中空の胴、下げの足は塗りつぶした胴と、形で分かれる
.cl__body.is-up { border: 2px solid $color; background: transparent; transform-origin: 50% 100%; }
.cl__body.is-down { border: 2px solid $tick-down; background: $tick-down; transform-origin: 50% 0; }

05買い・売りの深さが伸び縮みする板

画面で起きること

中央の約定値をはさんで、上が売り、下が買いの厚みです。棒は外側の端を起点に伸びるので、どちら側から積み上がっているのかが色を読まなくても分かります。

買い・売りの深さが伸び縮みする板
新しいタブで開く

05 · 買い・売りの深さが伸び縮みする板 — 計測 累積面積 5.564% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleXtransform-origintransition

コードの読み方

400msごとに数量を振り直して動いたフレーム18/23・累積面積5.564%で、取引画面の右側の板や、流動性を確かめる社内ツールに使えます。

// その行までに積み上がった量の棒。絶対配置なので文字のマスの場所を取らず、文字は position: relative で棒の上に載せる。
// 更新のたび transition が長さをつなぐ — 値が飛ばずに流れる
.ob__bar { position: absolute; top: 1px; bottom: 1px; left: 0; right: 0; transition: transform $dur-quick $ease-out; }
// 売り側は右端から左へ、買い側は左端から右へ伸びる — transform-origin が互いに反対。
// 中央を基準に伸ばすと棒の両端が一緒に動き、「どちら側から積み上がるのか」が消える。
// 端の角も互いに反対へ丸めて、色が読めなくても方向が見えるようにした
.ob__bar--ask { transform-origin: 100% 50%; background: rgba(255, 122, 94, .3); border-radius: $r-xs 0 0 $r-xs; }
.ob__bar--bid { transform-origin: 0 50%; background: rgba(61, 220, 151, .3); border-radius: 0 $r-xs $r-xs 0; }

06騰落率で色が変わるグリッドヒートマップ

どんな動きか

12銘柄の騰落率が、同じ大きさのタイルの色だけで並びます。値が変わっても色はカットで飛ばず、次の色へつながって流れます。

騰落率で色が変わるグリッドヒートマップ
新しいタブで開く

06 · 騰落率で色が変わるグリッドヒートマップ — 計測 累積面積 29.14% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@propertycolor-mixgrid-template-columns

どこを変えるか

アニメーション36個・累積面積29.14%とこの編でいちばん画面が動きますが、混ぜる色の上限を50%で止めてあるので、どのタイルでも文字はクリーム一色のまま読めます。

.hm__tile {
  // タイルの騰落率: プレビューは --p で今の値(--va)と次の値(--vb)の間を行き来し、
  // 実使用では JS がこの場所へインラインで --chg を書き込み、この規則を上書きする
  --chg: calc(var(--va) + (var(--vb) - var(--va)) * var(--p));
  // 0 付近は盤の色のまま。上へ行くほど上昇色、下へ行くほど下落色を混ぜる (1% あたり 11%、上限 50%)
  --up-mix: clamp(0%, calc(var(--chg) * 11%), #{$mix-cap});
  --down-mix: clamp(0%, calc(var(--chg) * -11%), #{$mix-cap});
  background-color: color-mix(in oklab, $tick-down var(--down-mix),
                    color-mix(in oklab, $tick-up var(--up-mix), $tick-panel));
  // 実使用で値が変わるとき、色がカットで飛ばずにつながる — 登録した数値の属性なので補間される
  transition: --chg $dur-base $easing;
  display: grid; align-content: center; justify-items: center; gap: 0;
  min-height: 32px; padding: $sp-1 0; border-radius: $r-xs;
}

07桁が回る大きな数字ボード

何が見えるか

評価損益の桁が、走行距離計のように縦へ回ります。3桁ごとの区切りと小数点は回らず、その場に立ったままです。

桁が回る大きな数字ボード
新しいタブで開く

07 · 桁が回る大きな数字ボード — 計測 累積面積 3.781% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateYtabular-numsoverflow

コードのどこを見るか

動いたフレーム12/23・累積面積3.781%で、右端の桁から3%ずつ遅らせて回すため、ダッシュボード上部の総資産カードに置いても数字が読めなくなりません。

// 桁のマス — 0 から 9 まで縦に積んだ帯を引き上げる。マスははみ出した部分を切り取る
.od__digit { display: block; overflow: hidden; width: .64em; height: $cell; }
.od__strip {
  display: block;
  transform: translateY(calc(var(--d) * -#{$cell}));
  // 実使用で値が変わるとここが回る。登録した数値の属性なので 9 から 0 へも続けて流れる
  transition: --d $dur-enter $easing;
}
.od__n { display: block; height: $cell; line-height: 1.15; text-align: center; }
// 区切りと小数点は回さない — その場に留まっていないと、桁がどこで切れるのかを目が見失う
.od__fixed { display: block; width: .3em; height: $cell; line-height: 1.15; }
// 先頭の 0 は薄く。マスはそのままに、読む桁だけをはっきり残す (値が 0 をまたぐとき実使用で現れる)
.od__digit.is-lead, .od__fixed.is-lead { opacity: .32; }

08設定した線を超えたら知らせるしきい値バッジ

画面で起きること

自分で決めた線を値が超えた瞬間だけ、鈴のバッジが出て輪が一度広がります。音は鳴らさず、「超過 / 未満」という文字と ▲▼ が同じことをもう一度言います。

設定した線を超えたら知らせるしきい値バッジ
新しいタブで開く

08 · 設定した線を超えたら知らせるしきい値バッジ — 計測 累積面積 2.406% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

aria-livebox-shadowsteps(1, end)

コードの読み方

ポインタを載せて3秒待つと文字の変わったマスが2つ・インラインスタイルの変わったマスが1つあり、動いたフレームは21/23でした — 価格アラートの設定画面や、在庫や温度のしきい値監視に置けます。

function paint() {
  var th = Number(rangeEl.value);
  var chg = (price - BASE) / BASE * 100;
  priceEl.textContent = money(price);
  dirEl.textContent = arrow(chg) + ' ' + pct(chg);
  dirEl.className = 'at__dir at__dir--next ' + (chg >= 0 ? 'is-up' : 'is-down');
  thEl.textContent = money(th);
  alertValEl.textContent = money(th);
  // ホルダーはトラックと同じ幅なので、translateX の % がそのままトラック上の位置になる (点自身の幅が基準にならない)
  dotEl.style.transform = 'translateX(' + posOf(price).toFixed(1) + '%)';
  lineEl.style.transform = 'translateX(' + posOf(th).toFixed(1) + '%)';
  // しきい値の通過は「前の状態と今の状態が線をはさんで分かれたとき」だけ一度発動する。
  // 同じ側にとどまっているあいだは、値がどれだけ動いても二度と鳴らない
  var over = price >= th;
  if (wasOver === false && over) { fire(); }
  wasOver = over;
  root.classList.toggle('is-over', over);
}

09接続状態と更新の遅れの表示

どんな動きか

点が呼吸しているあいだに、最後の更新から何秒たったかを数えます。状態は点の色だけでなく「ライブ / 切断 / 再接続中」という文字でも伝えます。

接続状態と更新の遅れの表示
新しいタブで開く

09 · 接続状態と更新の遅れの表示 — 計測 累積面積 0.926% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

visibilityStateanimation-delayopacity

どこを変えるか

同じ3秒で文字の変わったマスは4つ、累積面積0.926%とこの編でいちばん静かな部品ですが、タブが見えていないあいだはタイマーが数えないので、右上に置きっぱなしにできます。

function step() {
  if (root.classList.contains('is-demo')) { return; }
  // タブが見えていなければ数えない — 見えない画面の遅れの数字に意味はなく、戻ってきたとき
  // たまった更新が一度にあふれないようにもする (document.visibilityState)
  if (document.visibilityState === 'hidden') { return; }
  var since = Date.now() - lastAt;
  if (phase === LIVE) {
    // 五回受け取ったあと、値が来ない状況をまねる — 遅れがたまると状態が自分で切り替わる
    if (ticks > 0 && ticks % 5 === 0) {
      if (since > 2600) { phase = LOST; setState(LOST); }
    } else if (since > 1400) { feed(); }
  } else if (phase === LOST) {
    if (since > 4200) { phase = RETRY; setState(RETRY); }
  } else if (since > 6000) {
    phase = LIVE; setState(LIVE); feed();
  }
  showLag();
}

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

いちばん静かに壊れたのは、流れる帯の箱に width: 100% と書いた02でした。overflow: hidden ははみ出した部分を隠して見せるだけで、要素の max-content を縮めてはくれません。幅が決まらないままだと外側の格子が中身の最大幅まで広がり、480pxの舞台が1,047pxまで裂けて横スクロールが出ました。幅を min(340px, 84vw) に釘を打って直しています。04のツールチップも似た壊れ方をしました — 位置を px で書き込んでいたので、読み込み直さずに表示幅だけ480pxから320pxへ縮めると古い値が残り、盤の外へ出てしまったのです。resize で測り直し、max-width: 100% を添えて止めました。

二つ目は色です。下落色に手近な濃い赤 #c2361a を置いたところ、ティッカーの面 #231f28 の上で 2.96:1 しかなく、本文に必要な 4.5:1 に届きませんでした。#ff7a5e に替えて同じ面の上で 6.31:1 です(上昇の #3ddc97 は同じ面で 9.15:1)。06のタイルも同じ理由で、混ぜる比率を55%にすると #3e8163 の上のクリーム文字が 4.35:1 まで落ちるため、上限を50%(上昇 #3c775e の上で 4.93:1・下落 #8a4c45 の上で 6.16:1)で止め、文字色はどのタイルでもクリーム一色に揃えました。

落とし穴 症状 直し方
帯の箱に width: 100% 480pxの舞台が1,047pxに裂ける 幅を min(340px, 84vw) で固定
ツールチップの left を px で固定 320px幅で盤の外へ出る resize で測り直す
下落色に #c2361a #231f28 の上で 2.96:1 #ff7a5e へ(同じ面で 6.31:1)

一画面で複数の行が同時に更新され、どこを見ればよいか見失う問題は、通知 ui 9選の活動フィードでも同じ形で現れます。

三つ目は07で、六桁を同時に回したら代表画像がまるごとぼやけました。右端の桁から3%ずつ遅らせて回すと、どの瞬間も動いているのは一桁か二桁だけになり、残りの桁は読めるままでした。九つのソースをまとめた zip の鍵は m5tz6sq7 で、開くと項目番号ごとに、ブラウザでそのまま開ける版と React 版が対で入っています。

アクセシビリティ

方向を色だけで伝えている画面はひとつもありません。九つとも ▲▼ と +/− を数字の隣に必ず書き、09は点の横に「ライブ / 切断 / 再接続中」という文字を、08は「超過 / 未満」という文字を添えます。aria-live="polite" は08のお知らせ一行だけに付けました — 更新され続ける価格のマスに付けると、スクリーンリーダーが株価を延々と読み上げて何も操作できなくなります。点滅は WCAG 2.3.1 の毎秒3回以下に収めてあり、2秒のループの中で01が3回(毎秒1.5回)、08と09が2回(毎秒1回)、02と03は0回です。prefers-reduced-motion: reduce では光りと移動を止めて値の更新だけを残します — 01はフラッシュが消えて数字だけが入れ替わり、02の帯は先頭で止まったまま数字が変わり、03はずれが止まって JS が書き替える一枚の値だけが残り、04は最後のローソクが伸びきった形で立ち、05は棒の長さだけが更新され、06は色の流れと数字の切り替えが止まり、07は桁が回らずその場に立ち、08は超過の状態のまま輪が消え、09は呼吸と点滅が止まって状態の文字と秒数が残ります。09では document.visibilityState が hidden のあいだタイマーが数えないので、戻ってきたときに実際より長い待ち時間が表示されることもありません。

FAQ

証券会社のAPIキーがなくても本当に動きますか。

動きます。九つとも価格は mulberry32 という十行ほどの擬似乱数で作っていて、ネットワーク通信はひとつもありません。本物の値につなぐときは、その乱数を呼んでいる一か所を自分の取得処理に差し替えるだけで、表示側のコードはそのまま使えます。

リアルタイム ui は画面を開いたままでも重くなりませんか。

更新のたびに動かしているのは transform と opacity と背景色だけなので、レイアウトの計算はやり直されません。そのうえ九つとも setInterval の先頭で表示状態を確かめ、タブが見えていないあいだは何もしないので、裏で開いたままのタブが電池を削り続けることもありません。しきい値を超えたときに出す通知そのものの積み上がりと消え方は、トースト通知 UI 9選で九通りにまとめています。

横に流れる帯は無限ループのマーキーと同じものですか。

見た目は似ていても、中で解いている問題が違います。CSS 無限ループ マーキー9選が扱うのは切れ目なく回り続ける並べ方そのもので、こちらの02はそこに、ホバーでの停止と、一周が終わった瞬間だけ新しい銘柄を差し込む仕組みが乗ります。

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

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