jpskill.com
📦 その他 コミュニティ

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本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
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
🪟 Windows (PowerShell)
$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. 1. 下の青いボタンを押して animated-focus.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → animated-focus フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

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キー)が、キーボードで開いた後に機能しない
  • アニメーションクラスがないデモでは正常に動作する
  • アニメーションクラスがあるデモでは動作しない

根本原因

  1. fade-in-0 のようなCSSアニメーションは、要素を opacity: 0 から開始する
  2. focus() がレンダリング直後に呼び出されると、要素はまだ非表示である
  3. ブラウザは非表示要素へのフォーカスを拒否する
  4. フォーカスはコンテンツに移動せず、トリガーボタンに残る
  5. キーボードイベントはコンテンツではなくトリガーに送られる(開いている場合は何も起こらない)

コンソールデバッグからの証拠

// 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));
}

主要なポイント

  1. 最初に requestAnimationFrame を使用する - フォーカスを試みる前にCSSが適用されていることを確認します
  2. document.activeElement を確認する - フォーカスが実際に成功したかどうかを確認します
  3. 遅延を伴うリトライ - 20msの間隔でアニメーションの進行を許可します
  4. 試行回数の制限 - 5回の試行 = 最大100msの待ち時間で、一般的なアニメーションには十分です
  5. すべてのフォーカス関数に適用する - 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 focusElementfocusElementById 関数を含む
src/SummitUI/Components/Select/SelectContent.cs オープン時に FocusElementAsync を呼び出す
src/SummitUI/Components/DropdownMenu/DropdownMenuContent.cs メニューアイテムに対して FocusElementByIdAsync を呼び出す
src/SummitUI/Components/Popover/PopoverContent.cs ポップオーバーコンテンツのフォーカスを管理する

検討された代替アプローチ

  1. より長い初期遅延 - 最初のフォーカス試行前に50〜100msの遅延を使用できますが、顕著なラグが発生します
  2. アニメーション開始前にフォーカス - レンダリング順序の変更が必要で、複雑になります
  3. フォーカス中にアニメーションを無効にする - 視覚的なグリッチが発生します
  4. 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

  1. CSS animations like fade-in-0 start the element at opacity: 0
  2. When focus() is called immediately after render, the element is still invisible
  3. Browser rejects focus on invisible elements
  4. Focus stays on the trigger button instead of moving to content
  5. 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

  1. Use requestAnimationFrame first - Ensures CSS has been applied before attempting focus
  2. Check document.activeElement - Verify if focus actually succeeded
  3. Retry with delays - 20ms intervals allow animation to progress
  4. Limited attempts - 5 retries = 100ms max wait, enough for typical animations
  5. Apply to all focus functions - Both focusElement(element) and focusElementById(id) need this pattern

Functions Updated

  • floating.js:focusElement(element) - Used by SelectContent
  • floating.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

  1. Longer initial delay - Could use 50-100ms delay before first focus attempt, but adds noticeable lag
  2. Focus before animation starts - Would require changes to render order, complex
  3. Disable animation during focus - Would cause visual glitch
  4. CSS visibility instead of opacity - 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.