animated-focus
フローティング要素(セレクト、ドロップダウンメニューなど)の開閉アニメーション時に、キーボード操作でのナビゲーションが正常に動作しない問題を解決するためのノウハウをまとめたSkill。
📜 元の英語説明(参考)
This document captures learnings from fixing keyboard navigation issues when floating components (Select, DropdownMenu, Popover) have CSS open/close animations.
🇯🇵 日本人クリエイター向け解説
フローティング要素(セレクト、ドロップダウンメニューなど)の開閉アニメーション時に、キーボード操作でのナビゲーションが正常に動作しない問題を解決するためのノウハウをまとめたSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o animated-focus.zip https://jpskill.com/download/17178.zip && unzip -o animated-focus.zip && rm animated-focus.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17178.zip -OutFile "$d\animated-focus.zip"; Expand-Archive "$d\animated-focus.zip" -DestinationPath $d -Force; ri "$d\animated-focus.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
animated-focus.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
animated-focusフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
CSSアニメーションを用いたフォーカスマネジメント
このドキュメントは、フローティングコンポーネント(Select、DropdownMenu、Popover)がCSSによる開閉アニメーションを持つ場合に発生するキーボードナビゲーションの問題を修正した際の学びをまとめたものです。
問題点
フローティングコンテンツ要素が opacity: 0 から始まるCSSアニメーション(Tailwindの animate-in fade-in-0 など)を持つ場合、ブラウザは要素が非表示であるため、element.focus() の呼び出しを拒否することがあります。
症状
- マウスクリックではコンポーネントが正しく開く
- キーボード操作(矢印キー、Escapeキー)が、キーボードで開いた後に機能しない
- アニメーションクラスがないデモでは正常に動作する
- アニメーションクラスがあるデモでは動作しない
根本原因
fade-in-0のようなCSSアニメーションは、要素をopacity: 0から開始するfocus()がレンダリング直後に呼び出されると、要素はまだ非表示である- ブラウザは非表示要素へのフォーカスを拒否する
- フォーカスはコンテンツに移動せず、トリガーボタンに残る
- キーボードイベントはコンテンツではなくトリガーに送られる(開いている場合は何も起こらない)
コンソールデバッグからの証拠
// Selectを開いた後、キーボードイベントはコンテンツではなくトリガーに送られる:
Document keydown: ArrowDown Target: <button role="combobox" ...>
// アクティブな要素はコンテンツではなくトリガーである:
Active: BUTTON summit-select-...-trigger
解決策
フォーカスのリトライメカニズムを実装し、アニメーションが opacity: 0 を超えて進行するのを待ってから諦めるようにします。
JavaScriptの実装
// src/SummitUI/Scripts/floating.js
/**
* アニメーション要素のためにリトライメカニズムを用いて要素にフォーカスします。
* opacity:0 から始まるCSSアニメーションを持つ要素は、最初にフォーカスを拒否する可能性があります。
* これは、アニメーションが非表示状態を過ぎて進行できるように、20msの遅延で最大5回までフォーカスをリトライします。
* @param {HTMLElement} element - フォーカスする要素
*/
export function focusElement(element) {
if (!element) return;
function tryFocus(attempts) {
element.focus();
// フォーカスが成功せず、試行回数が残っている場合は、リトライする
if (document.activeElement !== element && attempts > 0) {
setTimeout(() => tryFocus(attempts - 1), 20);
}
}
// CSSの適用を待つため、最初の試行は1フレーム後に行う
requestAnimationFrame(() => tryFocus(5));
}
主要なポイント
- 最初に
requestAnimationFrameを使用する - フォーカスを試みる前にCSSが適用されていることを確認します document.activeElementを確認する - フォーカスが実際に成功したかどうかを確認します- 遅延を伴うリトライ - 20msの間隔でアニメーションの進行を許可します
- 試行回数の制限 - 5回の試行 = 最大100msの待ち時間で、一般的なアニメーションには十分です
- すべてのフォーカス関数に適用する -
focusElement(element)とfocusElementById(id)の両方にこのパターンが必要です
更新された関数
floating.js:focusElement(element)- SelectContent で使用floating.js:focusElementById(elementId)- DropdownMenuContent で使用
テスト
テストデモページと対応する Playwright テストに "With Animations" セクションを追加しました。
テスト用のCSSアニメーションクラス
/* tests/SummitUI.Tests.Manual/SummitUI.Tests.Manual/wwwroot/app.css */
@keyframes fadeInZoomIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeOutZoomOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
.animated-content[data-state="open"] {
animation: fadeInZoomIn 150ms ease-out forwards;
}
.animated-content[data-state="closed"] {
animation: fadeOutZoomOut 150ms ease-in forwards;
}
テストケース
各コンポーネント(Select、DropdownMenu、Popover)について:
| テスト | 検証内容 |
|---|---|
Animated*_ShouldOpen_OnEnterKey |
アニメーションが存在する場合、キーボードで開く |
Animated*_ShouldNavigate_WithArrowKeys |
アニメーションによるオープン後、矢印キーが機能する |
Animated*_ShouldSelect/Activate_OnEnterKey |
アニメーションによるオープン後、アイテムを選択/アクティブ化できる |
Animated*_ShouldClose_OnEscape |
Escapeキーでクローズアニメーションがトリガーされる |
アニメーションテストの実行
dotnet run --project tests/SummitUI.Tests.Playwright -- --treenode-filter '/*/*/*/Animated*'
関連ファイル
| ファイル | 目的 |
|---|---|
src/SummitUI/Scripts/floating.js |
focusElement と focusElementById 関数を含む |
src/SummitUI/Components/Select/SelectContent.cs |
オープン時に FocusElementAsync を呼び出す |
src/SummitUI/Components/DropdownMenu/DropdownMenuContent.cs |
メニューアイテムに対して FocusElementByIdAsync を呼び出す |
src/SummitUI/Components/Popover/PopoverContent.cs |
ポップオーバーコンテンツのフォーカスを管理する |
検討された代替アプローチ
- より長い初期遅延 - 最初のフォーカス試行前に50〜100msの遅延を使用できますが、顕著なラグが発生します
- アニメーション開始前にフォーカス - レンダリング順序の変更が必要で、複雑になります
- フォーカス中にアニメーションを無効にする - 視覚的なグリッチが発生します
- CSSの
opacityの代わりにvisibilityを使用する - アニメーションの作成方法の変更が必要になります
リトライメカニズムが選択された理由は次のとおりです。
- 任意のアニメーション期間で動作する
- CSSの作成方法の変更が不要
- パフォーマンスへの影響が最小限
- フォーカスが成功しなくても正常に失敗する
関連するパターン
このパターンは、bits-ui が Svelte コンポーネントでアニメーションによるプレゼンスを処理する方法と似ています。重要な洞察は、DOM 操作(フォーカスなど)は、CSS アニメーションがフォーカス可能な状態になるまで待つ必要がある可能性があるということです。
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Focus Management with CSS Animations
This document captures learnings from fixing keyboard navigation issues when floating components (Select, DropdownMenu, Popover) have CSS open/close animations.
The Problem
When floating content elements have CSS animations that start at opacity: 0 (like Tailwind's animate-in fade-in-0), the browser may reject element.focus() calls because the element is invisible.
Symptoms
- Component opens correctly with mouse clicks
- Keyboard navigation (arrow keys, Escape) doesn't work after opening with keyboard
- Works fine in demos without animation classes
- Broken in demos with animation classes
Root Cause
- CSS animations like
fade-in-0start the element atopacity: 0 - When
focus()is called immediately after render, the element is still invisible - Browser rejects focus on invisible elements
- Focus stays on the trigger button instead of moving to content
- Keyboard events go to trigger (which does nothing when open) instead of content
Evidence from Console Debugging
// After opening select, keyboard events go to trigger, not content:
Document keydown: ArrowDown Target: <button role="combobox" ...>
// Active element is trigger, not content:
Active: BUTTON summit-select-...-trigger
The Solution
Implement a retry mechanism for focus that allows the animation to progress past opacity: 0 before giving up.
JavaScript Implementation
// src/SummitUI/Scripts/floating.js
/**
* Focus an element with retry mechanism for animated elements.
* Elements with CSS animations starting at opacity:0 may reject focus initially.
* This retries focus up to 5 times with 20ms delays to allow the animation
* to progress past the invisible state.
* @param {HTMLElement} element - Element to focus
*/
export function focusElement(element) {
if (!element) return;
function tryFocus(attempts) {
element.focus();
// If focus didn't succeed and we have attempts left, retry
if (document.activeElement !== element && attempts > 0) {
setTimeout(() => tryFocus(attempts - 1), 20);
}
}
// First attempt after one frame to let CSS apply
requestAnimationFrame(() => tryFocus(5));
}
Key Points
- Use
requestAnimationFramefirst - Ensures CSS has been applied before attempting focus - Check
document.activeElement- Verify if focus actually succeeded - Retry with delays - 20ms intervals allow animation to progress
- Limited attempts - 5 retries = 100ms max wait, enough for typical animations
- Apply to all focus functions - Both
focusElement(element)andfocusElementById(id)need this pattern
Functions Updated
floating.js:focusElement(element)- Used by SelectContentfloating.js:focusElementById(elementId)- Used by DropdownMenuContent
Testing
Added "With Animations" sections to test demo pages and corresponding Playwright tests.
CSS Animation Classes for Testing
/* tests/SummitUI.Tests.Manual/SummitUI.Tests.Manual/wwwroot/app.css */
@keyframes fadeInZoomIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeOutZoomOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
.animated-content[data-state="open"] {
animation: fadeInZoomIn 150ms ease-out forwards;
}
.animated-content[data-state="closed"] {
animation: fadeOutZoomOut 150ms ease-in forwards;
}
Test Cases
For each component (Select, DropdownMenu, Popover):
| Test | What It Verifies |
|---|---|
Animated*_ShouldOpen_OnEnterKey |
Opens with keyboard when animations present |
Animated*_ShouldNavigate_WithArrowKeys |
Arrow keys work after animated open |
Animated*_ShouldSelect/Activate_OnEnterKey |
Can select/activate item after animated open |
Animated*_ShouldClose_OnEscape |
Escape triggers close animation |
Running Animation Tests
dotnet run --project tests/SummitUI.Tests.Playwright -- --treenode-filter '/*/*/*/Animated*'
Files Involved
| File | Purpose |
|---|---|
src/SummitUI/Scripts/floating.js |
Contains focusElement and focusElementById functions |
src/SummitUI/Components/Select/SelectContent.cs |
Calls FocusElementAsync on open |
src/SummitUI/Components/DropdownMenu/DropdownMenuContent.cs |
Calls FocusElementByIdAsync for menu items |
src/SummitUI/Components/Popover/PopoverContent.cs |
Manages focus for popover content |
Alternative Approaches Considered
- Longer initial delay - Could use 50-100ms delay before first focus attempt, but adds noticeable lag
- Focus before animation starts - Would require changes to render order, complex
- Disable animation during focus - Would cause visual glitch
- CSS
visibilityinstead ofopacity- Would require changes to how animations are authored
The retry mechanism was chosen because it:
- Works with any animation duration
- Doesn't require changes to CSS authoring
- Has minimal performance impact
- Fails gracefully if focus never succeeds
Related Patterns
This pattern is similar to how bits-ui handles animated presence in Svelte components. The key insight is that DOM operations (like focus) may need to wait for CSS animations to reach a focusable state.