告白
納品前に、4日くらい遊んでしまう癖がある。(1人で請負う案件)
意味もなく画面遷移して、意味もなくボタンを押して、自分の作ったUIを眺めて、UXにニヤニヤする。デレデレする。
昔はこれを「悪い癖」だと思って隠していました。今は工程表に計上してる。
要件定義 ⬜️⬜️
設計 ⬜️⬜️
実装 ⬜️
デレデレ ⬜️⬜️⬜️⬜️⬜️⬜️ ← ここ
デプロイ ⬜️
納品調整 ⬜️
ふざけてるように見えると思う。半分ふざけてる。でも残り半分は、かなり真面目な品質保証。
デレデレ中に何が起きてるか
自分の作ったモノで4日遊ぶと、必然的にこうなる。
- 一番ヘビーなユーザーが自分になる。 想定ユーザーの誰よりも先に、誰よりも長く使い込むことになる。
- 動作確認では踏まない操作を踏む。 テスト項目は「正しく使ったら正しく動くか」しか見えない。デレデレすることには目的がないから、変な順番でボタンを押す。戻るボタンを連打する。ありえない入力をする。つまり、本番のユーザーと同じ動きをする。
- 粗が「気になる」レベルで見つかる。 バグ未満の違和感——ここのアニメーションちょっと引っかかるな、この文言なんか冷たいな——は、義務で触ってたら流してしまう。惚れて触っていれば、気になって直してまう。
粗の磨き込み
具体例1: 見た目は変えず、Tab順だけ入れ替える
ポップアップの右下に [キャンセル] [決定] が並んでる。
見た目はこれで正しい。
でもキーボードでフォームを埋めて、最後にTabを押すと——次に当たるのはキャンセル。
そのとき作ったフォームは、大半の利用が決定に進むのに、一個余計に押さなければいけない。
Googleのサービスの一部のダイアログはここが賢くなっていて、フォームの直後にTabすると決定に当たることがある。だから真似した。
DOMの並びは 決定 → キャンセル にして、見た目だけ flex の order で入れ替える。
<!-- Tab順(=DOM順)は 決定 が先。見た目だけ後で入れ替える -->
<div class="actions">
<button type="submit" class="primary">決定</button>
<button type="button" class="ghost">キャンセル</button>
</div>
css.actions { display: flex; justify-content: flex-end; gap: 8px; }
.actions .primary { order: 2; } /* Tabは先。見た目は右へ */
.actions .ghost { order: 1; } /* Tabは後。見た目は左へ */
order は描画順しか変えない。Tab順はDOMのまま——ここがミソ。
見た目の [キャンセル] [決定] を保ったまま、キーボードだけ決定に直行させられる。
Tab順なんてマウス派は一生気づかない。
でも自分がキーボードで100回ポチポチ検証してると、この一手が効いてるのを感じる。
具体例2: 一瞬のローディングは、いっそ見せない
処理が速いと、スピナーが0.1秒くらいピカッと光って消える。
これが地味に不安になる。
「今の、一瞬なんか光ったな…ちゃんと成功したんか?」。
...バグではない。動いてる。でも触っていて気持ち悪い。
だから2段構えにした。
速く終わるならスピナーは出さない。出すと決めたら、逆に一瞬で消さない。
/**
* スピナーを「速い処理では見せず、見せるなら一瞬で消さない」よう制御する。
*
* @param {object} options
* @param {number} [options.delay=200] これより速く終わればスピナーは出さない (ms)
* @param {number} [options.minVisible=500] 一度出したら最低これだけ出す (ms)
* @param {() => void} options.show
* @param {() => void} options.hide
* @returns {{ start: () => void, stop: () => void }}
*/
function createSpinnerGate({ delay = 200, minVisible = 500, show, hide }) {
/** @type {ReturnType<typeof setTimeout> | null} */
let showTimer = null;
/** @type {number | null} */
let shownAt = null;
return {
start() {
showTimer = setTimeout(() => {
shownAt = Date.now();
show();
}, delay);
},
stop() {
if (showTimer) {
clearTimeout(showTimer);
showTimer = null;
}
if (shownAt === null) return; // まだ出してない = 一瞬で終わった。何も見せずに済む
const rest = Math.max(0, minVisible - (Date.now() - shownAt));
setTimeout(() => {
hide();
shownAt = null;
}, rest);
},
};
}
delay: 200 が「0.2秒はスピナーしていないフリ」、
minVisible: 500 が「0.5秒かかったフリ」。
自分の指が覚えてる気持ちよさの閾値を、そのまま数字にしただけ。
⋮
ついでにボタン内スピナーは、ラベルを丸ごと置き換えると幅が変わってガタつくので、ラベルは動かさず頭に小さいスピナーを添えるだけにした。
位置がズレないから、ガタつきが消える。
(末尾にスリードットの「...」をアニメーションで見せるのもアリだと思う)
テストしてくれる人もユーザーでさえも「0.2秒のチラつきが気になります」なんて報告は上げてくれない。
デレデレしながら、4日触ってるからこそ、腹が立って直す。
具体例3: 追加した行を「ニューッ」と出す
一覧に項目を追加する。パッと現れる。機能的には完璧。
でも一瞬、「あれ、今追加したやつどれ?」と目が探す。でもたいてい、まず1ミリも困らない。
ただ、ほんの少しだけ気持ちよくない。
だから新規行を、高さ0からふわっと開くようにした。
/* height: auto はトランジションできないので、
grid-template-rows の 0fr → 1fr で高さをアニメーションさせる */
.row-enter {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 240ms ease;
}
.row-enter.is-in { grid-template-rows: 1fr; }
.row-enter > * { overflow: hidden; } /* 潰れてる間はみ出さないように */
開き終わりに一瞬だけ背景をハイライトして、「これ」と視線を誘導する。
他にも
- 文言がいらつかないか、冷たい表現になっていないか
- タップできそうなのにタップできないモノはないか、区別できているか
- カードUIのカードの角丸のサイズ感
- カードUIを展開した時のアニメーション、カードそれ自体の形の調整
- 文字の太字箇所は誤操作を招かないか
- カラフルな色相的なバランス(機械的ではなく人間の目線で)
- わざわざ確認される削除UX(頻繁に削除するのであれば操作取消ボタンで)
- アニメーションの開きと閉じでの対称性(対称の方がたいてい気持ちよくない)
- 場合に寄ってはタップ操作をponterdown化
- 入力フォームの勝手な予測変換
など、ずっと触っていれば気になるところはどんどん出てきます。
要するにこれ、ドッグフーディング。ただし義務ではなく、いつも勝手にやっている。4日遊んでも飽きなかったら、そのUI/UXはたぶん大丈夫。さすがに1日目で嫌になれば、何かがおかしい。
自分の作ったモノに自分がデレデレできるかは、品質指標だと思ってる。
デレデレできないときは、危険信号
逆のパターンも書いておきます。
過去には、納品物が早く出来てもデレデレできなかったことがあります。
動く。要件も満たしてる。でも触っていて楽しくない。
自分の思う成果物に届いていないのが、触るたびに分かる。
そういうときは、だいたい設計のどこかで妥協してる。
デレデレは、その妥協を検知するセンサーでもある。
そのいうときには、設計から作り直したこともある。
まとめ
- 納品前に、目的なく自分の成果物で遊ぶ時間を工程に入れる
- 遊びは「正しい使い方」の外側を踏むから、テストの死角を潰す
- 自分がデレデレできるかどうかは、仕様書に書けないUI/UX, テストの品質指標
- デレデレできないときは、どこかで妥協してる
次回以降の記事で、なぜ私が成果物で4日もデレデレできるのかを書く予定です。