How to Debug Keyboard Trap in Mobile Apps

How to Debug Keyboard Trap in Mobile Apps: a hands‑on guide for developers and QA engineers

February 01, 2026 · 17 min read · Common Issues

How to Debug Keyboard Trap in Mobile Apps: a hands‑on guide for developers and QA engineers

A keyboard trap occurs when focus becomes stuck inside an interactive element, preventing users—especially those who rely on keyboards, switch controls, or assistive technologies—from moving away. In mobile apps this usually manifests as the soft keyboard staying on screen, the user being unable to dismiss it, or navigation gestures failing to leave a modal or custom view. The result is a broken accessibility experience, failed WCAG 2.1 §2.1.2 (No Keyboard Trap) compliance, and frustrated users who may abandon the app.

This guide walks you through the entire lifecycle of a keyboard‑trap bug: from reliable reproduction to root‑cause analysis, tooling, step‑by‑step diagnosis, concrete fixes, and preventive practices. Each section contains practical commands, log snippets, and tables you can copy into your workflow. The techniques apply to native Android, native iOS, and hybrid apps that embed WebViews or third‑party SDKs.

---

Understanding Keyboard Trap in Mobile Apps

What is a keyboard trap?

In accessibility terminology a keyboard trap is a state where keyboard focus cannot be moved out of a component using standard navigation keys (Tab, Shift‑Tab, arrow keys, or equivalent swipe gestures). On mobile devices the “keyboard” is the soft input method, but the principle is identical: focus is locked inside a view, dialog, or web‑page overlay, and the system’s focus‑change events are ignored or swallowed.

Why it matters for accessibility and UX

Common symptoms

SymptomTypical triggerObservable effect
Soft keyboard remains visible after tapping outside an inputModal dialog that does not release focusKeyboard overlaps background, back button does nothing
Focus cycles between two EditTextsCustom view that overrides onKeyPreIme incorrectlyUser cannot navigate to other fields or the app bar
TalkBack/VoiceOver reads same element repeatedlyFocus loop in a recycler view itemScreen reader stuck, user hears same announcement
Switch Control cannot advanceA full‑screen loading spinner that captures touch eventsSwitch scan pauses, user must force‑close app
Hardware keyboard arrows do not move focusActivity set to windowIsFloating without proper focus initArrow keys behave as if no focusable view exists

---

Root Causes of Keyboard Trap

Focus management failures

The most frequent cause is a custom view or activity that incorrectly consumes focus‑change events. Examples:

Modal dialogs and custom views

Dialogs that appear as separate windows (e.g., AlertDialog, bottom sheets) must explicitly set android:windowIsFloating and ensure the decorated view hierarchy includes at least one focusable element that can receive the initial focus. If the dialog’s decor view is set to notFocusable or its background consumes all touch events, the IME may stay anchored to the underlying activity.

Input method editor (IME) interactions

IME state changes (onStartInputView, onFinishInputView) are sometimes mishandled:

Third‑party libraries and webviews

---

Reproducing Keyboard Trap Reliably

Manual reproduction steps on Android

  1. Launch the app on a device or emulator (API 21+).
  2. Navigate to the screen suspected of containing the trap (e.g., login form).
  3. Tap an EditText to bring up the soft keyboard.
  4. Perform the action that should dismiss the keyboard (tap outside, press back, close a modal).
  5. Observe whether the keyboard remains visible and whether you can move focus to another element using Tab (if a hardware keyboard is connected) or swipe gestures with TalkBack.

Manual reproduction steps on iOS

  1. Run the app on a physical device or simulator (iOS 13+).
  2. Activate a text field to show the keyboard.
  3. Trigger the UI flow that should hide the keyboard (e.g., tap “Done”, swipe down, dismiss a pop‑up).
  4. Check if the keyboard stays on screen and whether VoiceOver can move focus away using the rotor or swipe gestures.

Automated reproduction with SUSA

SUSA’s autonomous explorer can surface keyboard traps without scripts:


# Install the agent
pip install susatest-agent

# Point SUSA at an APK or app URL
susatest run --app ./myapp.apk --personas curious impatient --max-depth 5

During exploration SUSA logs any instance where the soft keyboard remains visible for >2 seconds after a tap outside an input field, flagging it as a potential trap. The resulting report includes a video clip, the exact UI hierarchy, and the sequence of actions that led to the lock.

Creating a minimal repro case

When the trap is intermittent, strip the app down to the smallest possible activity:

A minimal repro makes log analysis faster and helps confirm whether the issue lies in your code or a third‑party component.

---

Diagnostic Tools and Signals

Android Logcat and debug bridge

Focus‑related logs are emitted by the ViewRootImpl and InputMethodManager services. Enable verbose tags:


adb logcat ViewRootImpl:V InputMethodManager:V ActivityManager:V *:S

Look for lines like:


I/ViewRootImpl: [focusChanged] oldFocus=android.widget.EditText{b42c1a0 VFED..C.. .......I. 0,0-720,1200} newFocus=null
W/InputMethodManager: Ignoring event: focus not gained within 2000ms

If newFocus stays null while the keyboard is still shown, the system thinks no view has focus.

iOS Console and Instruments

On iOS, the UIFocusSystem posts notifications. In Console, filter for UIFocusSystem:


predicate: processName contains "MyApp" && eventMessage contains "UIFocusSystem"

Key messages:


<UIFocusSystem: 0x102800c00> Focus update: focusedView changed from <UITextField: 0x10480a200> to <UIView: 0x10480b400> (reason: UIFocusUpdateReasonUserInitiated)

If the focused view never changes after a tap outside, the system is stuck.

Use Instruments → Automation to record a script that taps an input field, waits, then taps elsewhere, and check the focusedElement property.

Accessibility inspection tools (TalkBack, VoiceOver)

Profilers: CPU, memory, input event tracing

Using adb shell input events and uiautomator

You can synthesize key events to test focus escape:


# Send a TAB key event (keycode 61) to move focus forward
adb shell input keyevent 61
# Send BACK to dismiss keyboard
adb shell input keyevent 4

If after a series of TAB events the focus never leaves the EditText, the trap is confirmed.

uiautomator dump provides the current view hierarchy:


adb shell uiautomator dump /tmp/window.xml && cat /tmp/window.xml

Search for focused="true" attributes; if they remain on the same node after you issue a tap elsewhere, focus is locked.

Using Xcode's accessibility inspector

Open Xcode → Open Developer Tool → Accessibility Inspector. Point the inspector at the running app; the “Focused Element” field updates in real time. If it stays on a UITextField after you tap a button that should dismiss the keyboard, you have a trap.

---

Step‑by‑Step Diagnosis Workflow

1. Gather reproduction steps

Document the exact sequence: screen, input field, action that should dismiss keyboard, and any preceding dialogs or asynchronous calls (e.g., network request that shows a loading spinner).

2. Capture logs around focus change events

Start a timed logcat capture that begins before the reproduction and ends a few seconds after the expected dismissal:


adb logcat -c   # clear buffer
adb logcat ViewRootImpl:V InputMethodManager:V ActivityManager:V > focus_log.txt &
# perform reproduction
sleep 5
kill %1   # stop background logcat

Search focus_log.txt for focusChanged lines. Note the timestamps of the last successful focus change and the first occurrence where newFocus is null while the keyboard is still shown (you can correlate with InputMethodManager: showing soft input).

3. Identify the offending view or window

From the log, locate the package/class name of the view that last had focus. Then use uiautomator dump at the moment the trap is active to see the full hierarchy. Look for:

4. Verify focus escapes via accessibility service

Enable TalkBack and use the “Explore by touch” gesture to move focus manually. If TalkBack can move focus away, the trap is limited to touch navigation; if not, the issue is deeper in the focus manager.

5. Isolate the problematic code path

Set breakpoints or log statements in:

If you observe that onFocusChange is called with hasFocus = false but the view immediately calls requestFocus() again, you have found the loop.

6. Fix and verify with regression matrix

Apply the fix (see next section), then run the reproduction steps again. Use the same log capture to confirm that a proper focusChanged event with a non‑null newFocus occurs after the dismissal action. Add the scenario to your automated UI test suite (e.g., Espresso or XCTest) to prevent regression.

---

Fixing Common Causes

Fixing focus loops in custom views

Problem: A custom view overrides dispatchKeyEventPreIme and returns true for KEYCODE_BACK, preventing the IME from receiving the back key.

Solution:


class CustomEditText @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = None, defStyle: Int = 0
) : AppCompatEditText(context, attrs, defStyle) {

    override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
        // Let the system handle BACK first; only consume if we have custom logic
        if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_DOWN) {
            // Example: hide custom popup, then allow system to proceed
            if (popupIsShowing) {
                dismissPopup()
                return true   // we consumed it, but we already handled popup
            }
        }
        return super.dispatchKeyEventPreIme(event)
    }
}

Key points: call super for cases you don’t handle, and never swallow the event without a clear reason.

Properly dismissing modal dialogs

Problem: A full‑screen dialog created with DialogFragment uses setCancelable(false) and never calls dismiss() when the user taps outside, leaving the dialog window as the focused window.

Solution: Ensure the dialog’s decor view has at least one focusable element and that the dialog is dismissed on outside taps:


<!-- dialog_layout.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/dialog_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusable="true"
    android:focusableInTouchMode="true">
    <!-- content -->
</LinearLayout>

class MyDialog : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return requireContext().let { ctx ->
            AlertDialog.Builder(ctx)
                .setView(R.layout.dialog_layout)
                .setCancelable(true)   // allow back press to dismiss
                .create()
        }.apply {
            window?.setBackgroundDrawableResource(android.R.color.transparent)
            // optional: set focus to first EditText when shown
            setOnShowListener { dialog ->
                dialog.window?.currentFocus?.clearFocus()
                val firstEdit = dialog.findViewById<EditText>(R.id.first_input)
                firstEdit?.requestFocus()
            }
        }
    }
}

Handling IME state changes

Problem: An app hides the keyboard with inputMethodManager.hideSoftInputFromWindow(window.attributes.token, 0) but never clears the focused EditText. When the user taps another field, the IME thinks focus is still on the original field and does not show the keyboard.

Solution: Pair hide with clear focus:


fun hideKeyboardAndClearFocus(view: View?) {
    view?.clearFocus()
    val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    imm.hideSoftInputFromWindow(view?.windowToken, 0)
}

Call this function whenever you programmatically dismiss the keyboard (e.g., after a successful form submit).

Updating third‑party SDKs

If logs show that a class from com.thirdparty.sdk appears in the focus change stack trace, check the SDK’s changelog for known focus‑trap issues. Often a newer version fixes the problem by correctly setting windowIsFloating or by not overriding dispatchKeyEvent.

Procedure:

  1. Identify the SDK version in build.gradle.
  2. Review the SDK’s issue tracker or release notes for keywords “focus”, “IME”, “window”.
  3. Upgrade to the latest stable version.
  4. If upgrade is not possible, wrap the SDK’s activity in a transparent proxy that forwards focus events:

class FocusProxyActivity : AppCompatActivity() {
    private lateinit var sdkDelegate: ThirdPartySdkActivity

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sdkDelegate = ThirdPartySdkActivity()
        sdkDelegate.delegate = this   // custom interface to forward callbacks
    }

    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        // forward to SDK then to super
        return sdkDelegate.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
    }
}

WebView focus trapping solutions

Problem: A page loaded in a WebView contains a modal

with tabindex="-1" that traps focus when the user tabs. The WebView forwards the trap to the host app because the WebView itself never loses focus.

Solution:


<script>
function trapFocus() {
    const focusableEls = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
    const first = focusableEls[0];
    const last = focusableEls[focusableEls.length - 1];
    modal.addEventListener('keydown', e => {
        if (e.key === 'Tab') {
            if (e.shiftKey) { // shift + tab
                if (document.activeElement === first) {
                    e.preventDefault();
                    last.focus();
                }
            } else { // tab
                if (document.activeElement === last) {
                    e.preventDefault();
                    first.focus();
                }
            }
        }
    });
}
</script>

webView.webChromeClient = object : WebChromeClient() {
    override fun onCreateWindow(view: WebView?, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?): Boolean {
        val newWebView = WebView(context)
        newWebView.layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        // add to a container that is not focusable
        container.addView(newWebView)
        WebView.WebViewTransport transport = obj
        transport.webView = newWebView
        resultMsg.obj = transport
        resultMsg.sendToTarget()
        return true
    }
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.evaluateJavaScript("document.activeElement?.blur();") { _, _ in }
}

---

Prevention Strategies

Coding guidelines for focus management

Unit and UI tests for focus escape


onView(withId(R.id.username))
    .perform(click())
    .closeSoftKeyboard()
onView(withId(R.id.submit))
    .perform(pressKey(KeyEvent.KEYCODE_ENTER))
assertThat(onView(withId(R.id.username)), isNot(isFocused()))

XCTAssertFalse(usernameField.isFirstResponder, "Keyboard should be dismissed")

Add these tests to your CI pipeline so any regression is caught on pull request.

Integrating accessibility checks in CI

Using automated exploration tools like SUSA

Schedule a nightly SUSA run against your latest build artifact:


susatest run --app ./app-release.apk --personas elderly power_user --timeout 300 --output ./susa_report.json

The report includes a “Keyboard Trap” section with video evidence and a stack trace. Treat any finding as a blocker for release.

Code review checklist

ItemWhat to look for
Focus listenersDoes any OnFocusChangeListener immediately call requestFocus() on the same view?
Key event overridesAre dispatchKeyEventPreIme, dispatchKeyEvent, or onKeyDown returning true without calling super?
Dialog creationIs the dialog’s decor view focusable? Is setCancelable(true) used unless explicitly needed?
IME hide callsIs hideSoftInputFromWindow always paired with clearFocus()?
WebView contentDoes any loaded HTML contain a focus trap (modal with tabindex="-1" and no escape mechanism)?
Third‑party initDoes any SDK launch a full‑screen activity or window that lacks a focusable initial view?

---

Real‑World Examples and Edge Cases

Example 1: Custom PIN entry view causing trap

A fintech app implemented a PIN entry as a row of six EditText widgets, each limited to one character. The custom widget overrode onKeyPreIme to move focus to the next box when a digit was entered, but when the user pressed the backspace on the first box it called clearFocus() without moving focus elsewhere. The result: focus stayed on the PIN container still‑ show. Fix: In the InputMethodManager` never received a focus change, so the keyboard remained visible.

Fix: In the backspace case, explicitly request focus on the previous box (if any) or on the parent container that can receive focus.

Example 2: In‑app webview with login modal

A travel app loaded a third‑party payment gateway inside a WebView. The gateway’s HTML used a Bootstrap modal that trapped focus inside the modal when the user tabbed. Because the WebView itself never lost focus, the host app appeared to have a keyboard trap after the payment sheet closed.

Fix: Added a JavaScript interface that called webView.evaluateJavascript("document.activeElement.blur();") after the modal’s hidden event, returning focus to the WebView’s host view.

Example 3: Third‑party analytics SDK stealing focus

An analytics library initialized a foreground service that displayed a persistent notification with a custom action view (RemoteViews) containing a button. The notification’s window was flagged as type="TYPE_APPLICATION_OVERLAY" and set focusable="true" but had no focusable children. When the notification appeared, the system transferred focus to this empty window, causing the soft keyboard to stay locked on the last EditText.

Fix: Updated the SDK to version 2.4.1, which corrected the window flags. When an immediate upgrade wasn’t possible, the app added a BroadcastReceiver for NotificationListenerService that called cancelAllNotifications() on the problematic package during app start.

Example 4: Edge case – multi‑window mode on foldables

On a Samsung Galaxy Fold in multi‑window active mode, a dialog that used getWindow().setType(WindowManager.LayoutParams.TYPE_PHONE) (deprecated) was not re‑laid out when the window was resized. The dialog’s decor view retained focus even though its bounds were off‑screen, making the keyboard appear stuck.

Fix: Replaced the deprecated call with getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) and ensured the dialog used WindowManager.LayoutParams.TYPE_APPLICATION.

Example 5: Production‑only trap due to remote config

A feature flag rolled out a new “quick‑reply” bar that appeared as a transient view at the bottom of the screen. The bar was implemented as a FrameLayout with android:windowIsTranslucent="true" and android:background="@null". Under certain remote‑config values, the bar’s setEnabled(false) was called before it was added to the hierarchy, leaving the view in a disabled but focusable state. The IME could not find a focusable target and stayed visible.

Fix: Ensured the view was added to the hierarchy before any enable/disable calls, and added a unit test that asserts the view’s isEnabled state after inflation.

---

Test Matrix for Keyboard Trap Verification

ScenarioAndroid (API)iOS (version)Expected resultPass/Fail criteria
Tap EditText → press back21‑3313‑17Keyboard dismisses, focus moves to previous view or activity barfocusChanged logs show newFocus ≠ null within 500 ms
Tap EditText → tap outside (outside any modal)21‑3313‑17Keyboard dismisses, focus clearsNo focused="true" on any EditText after tap
Open modal dialog → tap EditText inside → dismiss dialog21‑3313‑17Keyboard remains visible while dialog open, dismisses after dialog closedWhile dialog open: keyboard visible; after dismiss: keyboard gone
Custom PIN view → enter digit → backspace on first field21‑3313‑17Focus moves to previous field or parent; keyboard staysNo focus loop; focusChanged shows movement
WebView loads page with modal trap → tap inside → close modal21‑3313‑17Keyboard dismisses, focus returns to host appAfter modal close, focused host view ≠ null
Third‑party SDK shows full‑screen interstitial → tap EditText → close interstitial21‑3313‑17Keyboard dismisses, focus returnsNo stuck keyboard; interstial removal triggers focus change
Device rotated while keyboard visible21‑3313‑17Keyboard remains visible, focus adapts to new orientationNo loss of focus; orientationChange logs present
TalkBack enabled → swipe to leave EditText21‑3313‑17Focus moves away, keyboard may stay (expected)TalkBack announces new element; no crash

Run this matrix on every release candidate. Automate the steps with Espresso/UI Tests and attach the logcat/syslog capture as an artifact for audit.

---

Quick Reference Checklist

---

Closing Takeaways

Keyboard traps are a subtle but high‑impact accessibility bug that can slip through functional testing because they often appear only under specific interaction sequences or device configurations. By combining reliable reproduction steps, targeted logging, a disciplined diagnosis workflow, and concrete fixes for the most common sources—focus loops, modal mishandling, IME state errors, and third‑party or WebView issues—you can eliminate these traps before they reach users.

Make focus‑escape verification a regular part of your test matrix, leverage automated explorers like SUSA to surface traps early in development, and embed accessibility checks in your CI pipeline. With these practices in place, you’ll not only satisfy WCAG 2.1.2 but also deliver a smoother, more predictable experience for every user, whether they rely on a touchscreen, a keyboard, switch control, or a screen reader.

---

*References*

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free