Web開発
ツールチップには遅延が必要、そしてその後、その遅延をスキップする必要がある
Tooltips Need a Delay, and Then They Need to Skip It (blog.master.dev)
要約
この記事では、ツールチップのUI/UXにおける一般的な問題点と、その解決策について解説しています。カーソルを素早く移動させた際にツールチップが頻繁に表示されるのを防ぐために遅延を設定すると、要素間を移動する際の操作感が悪化するという課題に対し、一定時間内に他の要素にホバーした場合に遅延をスキップする「ウォームウィンドウ」というパターンを提案しています。
全文翻訳
ツールチップには遅延が必要、そしてその後、その遅延をスキップする必要がある
Abhishek Jakhar 2026年8月13日
私のプロダクトであるFrontPrepの質問一覧ページでは、面接の質問の横に会社のロゴを表示しています。ロゴにカーソルを合わせると、ツールチップに会社名が表示されます。
数日間、あることが私を悩ませていました。ページ上でカーソルを移動させただけで、ツールチップが次々と即座に表示されるのです。原因は単純でした。ツールチップの遅延を0に設定していたため、カーソルがロゴにホバーした瞬間に表示されていました。
この問題を解決するために、200ミリ秒のトランジション遅延を追加しましたが、これは新たな問題を生み出しました。ユーザーインターフェースを見ると、同じ面接の質問が複数の会社で聞かれるため、隣接して配置されているロゴがいくつかあります。あるロゴから別のロゴに移動するたびに、別の会社のロゴにホバーするたびに同じ200ミリ秒の遅延を待たなければならなくなり、これは鈍重に感じられ、ユーザーエクスペリエンスを低下させました。
この記事はツールチップの構築についてではありません。それは、ブラウザのツールバーや様々なウェブサイトで見られる、人々が気づいていない一つの小さなインタラクションパターンについてです。私のウェブサイトでの変更前後の体験を示す短いビデオがあります。これにより、問題をより良く理解するのに役立ちます。
変更前(200ミリ秒の遅延なし)
変更後(200ミリ秒の遅延あり&即時ツールチップ)
解決策
私の解決策を3つの部分に分けます。
1. ロゴにカーソルを合わせます。ツールチップは表示されるまで200ミリ秒待ちます。
2. ツールチップが閉じ、300ミリ秒のタイマーが開始されます。私はこれをウォームウィンドウまたはウォームページと呼びます。
3. この300ミリ秒のタイマーが実行中、つまりページがウォームな間に別のロゴにカーソルを合わせた場合、ツールチップは待機やアニメーションなしで即座に開きます。
300ミリ秒のタイマーが期限切れになると、すべてが通常に戻り、ページはコールドになります。再びロゴにカーソルを合わせた場合、次のツールチップは再び200ミリ秒待つ必要があります。
このステップがないと、最初に開いたツールチップはページ全体の遅延を永久に無効にしてしまいます。
ホバー → 200ミリ秒待機 → ツールチップが開く(ページはウォームになる)
離れる → ツールチップが閉じる → 300ミリ秒のクールダウン
├─ クールダウン中に別のツールチップにホバー → 即座に開く、ページはウォームのまま
└─ クールダウン終了 → ページはコールド、200ミリ秒の待機が戻る
FrontPrepでは、ツールチップはRadixとMotionで構築されています。よりシンプルなReactバージョンでこのパターンを以下に示し、記事の後半ではこのアイデアをClaudeスキルに組み込み、コードベースを監査できるようにします。
JavaScriptを一切使用せずに基本的なUXを再現するフォローアップ記事はこちらです。
コードの仕組み
カーソルを移動する際に起こる順序と同じ順序でコードを説明しようと思います。
ステップ1:ロゴにカーソルを合わせる
ロゴでonMouseEnterがトリガーされると、この関数が呼び出されます。
function handleEnter() {
// `tooltips` は、コンポーネント間でisWarm状態を追跡するuseContext変数です
If (tooltips.isWarm) {
show();
return;
}
// `openTimer` はuseRef変数なので、.currentで値の設定/アクセスができます。
openTimer.current = setTimeout(show, tooltips.openDelay);
}
ホバー時、このコンポーネントは一つの質問をします:ページはウォームか?ウォームなら、ツールチップは即座に開きます。そうでなければ、200ミリ秒のタイマーを開始して待ちます。
このtooltipsオブジェクトがどこから来たのか不思議に思うかもしれません。これは、最後のステップで説明するTooltipProviderから来ています。
ステップ2:ツールチップが開く
ツールチップを開く処理を行うshow関数がこれです。
function show() {
setInstant(tooltips.isWarm);
setOpen(true);
tooltips.markOpened();
}
これは3つのことを行います:
1. tooltips.isWarmの値をinstantという状態変数にコピーします。
2. ツールチップを開きます。
3. ツールチップが開いたことをプロバイダーに通知し、ページをウォームにします。
ポイント1に戻ると、tooltips.isWarmが返す値をinstantフラグにコピーしたのは、このinstantフラグがツールチップにデータ属性として追加され、CSSがエントリトランジションをスキップするために使用するためです。
.tooltip[data-instant="true"] {
transition-duration: 0ms;
}
これが、ページがウォームなときにツールチップがアニメーションや遅延なしで開く理由です。
ステップ3:ロゴから離れる
ロゴでonMouseLeaveがトリガーされると、この関数が呼び出されます。
function handleLeave() {
clearTimeout(openTimer.current);
If (!open) return;
setOpen(false);
setInstant(false);
tooltips.markClosed();
}
まず、ツールチップが現在開いているかどうかを知る必要があります。カーソルが50ミリ秒でロゴを横切ったとしましょう。これは200ミリ秒よりはるかに短いため、handleEnterによって開始されたタイマーはまだ実行中で、ツールチップはまだ開いていません。handleLeaveのclearTimeoutはそのタイマーをキャンセルするため、ツールチップは決して開きません。そして、(!open) return; は関数をそこで停止させます。なぜなら、開かなかったツールチップには閉じるものも、開始するクールダウンもないからです。
最初のビデオでは、スワイプがその経路上のすべてのツールチップを開きました。今では、同じスワイプはどのツールチップも開きません。
ツールチップが開いている場合、これはカーソルを200ミリ秒以上ロゴに置いた場合、またはページがウォームで、ホバー時にツールチップが即座に開いた場合に発生します。ツールチップを閉じ、プロバイダーに通知します。プロバイダーは300ミリ秒のクールダウンタイマーを開始します。このタイマーが終了する前に次のロゴにホバーすると、ツールチップは即座に開きます。
ステップ4:プロバイダー
const warm = useRef(false);
const cooldownTimer = useRef(null);
useEffect(() => () => clearTimeout(cooldownTimer.current), []);
const tooltips = useMemo(
() => ({
openDelay,
isWarm: () => skipWhenWarm && warm.current,
markOpened() {
warm.current = true;
clearTimeout(cooldownTimer.current);
},
markClosed() {
clearTimeout(cooldownTimer.current);
cooldownTimer.current = setTimeout(() => {
warm.current = false;
}, warmFor);
},
}),
[openDelay, warmFor, skipWhenWarm]
);
これは、このパターン全体を機能させるために必要な共有状態です。ページはウォームまたはコールドのいずれかです。
もし気づけば、markOpenedは保留中のクールダウンをキャンセルします。これが、1つのロゴから別のロゴに移動する際にページをウォームに保つ理由です。基本的に、新しいツールチップは前のツールチップによって開始されたクールダウンをキャンセルします。
skipWhenWarmフラグについて混乱する必要はありません。これはデモのオン/オフ切り替えのためだけです。これをオフにすると、プロバイダーは常にコールドになり、前の動作を確認するのに役立ちます。
もう一つの重要な詳細は、isWarmがReactの状態ではなくrefであることです。なぜなら、それを変更してもページ上のすべてのツールチップが再レンダリングされるわけではないからです。このrefはイベントハンドラ内でのみ読み取られます。
なぜ200ミリ秒のタイマーなのか?
タイマーを150ミリ秒未満にすると、カーソルがツールチップトリガーを通過する際に、ほとんどの場合ツールチップが開いてしまいます。タイマーを250ミリ秒より長くすると、ホバー操作は鈍重で壊れているように感じられます。したがって、この場合200ミリ秒は良い、バランスの取れた数値です。
判断とセンス
AIにツールチップを構築するように依頼すると、完全に機能するツールチップを構築しますが、最終的に、構築されたツールチップが価値があるかどうかを決定するのはあなた、人間です。なぜなら、機能するツールチップと洗練されたツールチップを分ける詳細があり、それは機能する製品と洗練された製品を分けるのと同じ方法です。
AIは、あなたが指示した場合にのみ洗練された製品を構築できます。そして、経験、間違い、練習を長年積んで得られたセンスと判断力をあなたが持っている場合にのみ、指示できます。しかし、他のエンジニア/デザイナーのスキルを利用して、洗練された製品のセンスを得て、最終的にあなた自身のセンスを開発しながら構築することができます。色、アクセシビリティ、フォーム、アニメーション、タイポグラフィなどに関するスキルを生成/使用できます。堅実な出力を生成できますが、それはプロダクションレディであることを意味するわけではありません。最終的に、あなたの基準とセンスを満たす場合にのみ、それを判断して出荷するためにあなたはまだそこにいます。
スキル
私が直面した上記の問題を解決するためにClaudeスキルを作成しました。同様のスキルを構築したり、以下に書かれたものをコピーして実行してコードベースを監査したりできます。