Skip to content
Development
Skill

/compose-focus-navigation

Use when writing or reviewing Jetpack Compose UI for TV, keyboard, desktop, accessibility focus, D-pad navigation, FocusRequester, focusProperties, key events, or initial focus behavior.

From plugin
chrisbanes-skills
1k18 skills
Install
$ npx -y skills add chrisbanes/skills --skill compose-focus-navigation --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/compose-focus-navigation

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when writing or reviewing Jetpack Compose UI for TV, keyboard, desktop, accessibility focus, D-pad navigation, FocusRequester, focusProperties, key events, or initial focus behavior.

SKILL.md

compose-focus-navigation.SKILL.md
name: compose-focus-navigation
description: Use when writing or reviewing Jetpack Compose UI for TV, keyboard, desktop, accessibility focus, D-pad navigation, FocusRequester, focusProperties, key events, or initial focus behavior.

Compose: focus navigation

Core principle

Focus is stateful UI behavior: make targets and exceptional edges explicit, then drive and verify it through the user's keyboard, D-pad, or remote input.

Procedure

1. Start with components that already participate in focus. Add a hook only for a requested behavior:

| Need | Add | |---|---| | Normal button/text field/clickable focus | Nothing extra; use the focusable component | | Programmatic initial/restored focus | `FocusRequester` + `Modifier.focusRequester(...)` | | Visual or state reaction to focus changes | `Modifier.onFocusChanged { ... }` | | Custom interactive surface that is not already focusable | `Modifier.focusable()` plus role/semantics as appropriate |

2. Request initial or restored focus from `LaunchedEffect`, keyed to the condition that makes the target present. For lazy content, keep requesters by stable item id and request only after the item is composed. Inside `AnimatedContent`, use the content lambda's target consistently for rendered identity, tags, requester ownership, and the effect key; captured outer state gives outgoing and incoming content the same identity. 3. Keep default spatial search unless a concrete edge, jump, or trap is wrong. Encode only those exceptions with `focusProperties`. 4. Handle keys only for behavior that is not normal click or traversal. Consume exactly the handled event; throttle rapid D-pad work at its expensive owner, not across the screen. 5. Restore by semantic identity after refresh: retain the focused id when it exists, otherwise choose a deterministic fallback. 6. Test with one concrete key/D-pad interaction and focused semantics. When the behavior under review is user-triggered, do not substitute direct state mutation or leave the input conditional. Use screenshots only for the focus appearance. 7. Finish when all intentional targets and exceptional edges are encoded, loading/refresh behavior has a stable focus policy, and tests use the same input model as users.

For example, request and observe focus only when both behaviors are required:

val requester = remember { FocusRequester() }

Button(
    onClick = onClick,
    modifier = Modifier
        .focusRequester(requester)
        .onFocusChanged { state -> isFocused = state.isFocused },
) {
    Text("Play")
}

Call focus requests from an effect, not the composable body:

val initialFocus = remember { FocusRequester() }

LaunchedEffect(initialFocus) {
    initialFocus.requestFocus()
}

If the target appears after loading, key the request to the condition:

LaunchedEffect(items.isNotEmpty()) {
    if (items.isNotEmpty()) {
        firstItemRequester.requestFocus()
    }
}

Use `focusProperties` only when default spatial search is wrong:

Modifier.focusProperties {
    up = headerRequester
    down = firstRowRequester
    left = FocusRequester.Cancel
}

Too many hard-coded links create stale focus graphs. For special key behavior, consume only the handled event:

Modifier.onPreviewKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp && event.key == Key.Back) {
        onBack()
        true
    } else {
        false
    }
}

Test focus through user input:

composeTestRule.onNodeWithTag("screen").performKeyInput {
    pressKey(Key.DirectionDown)
}

composeTestRule.onNodeWithTag("play-button").assertIsFocused()

Broader test-shape choices are in [Compose UI testing patterns](../compose-ui-testing-patterns/SKILL.md).

Read more
Ships withchrisbanes-skills

A set of skills for Kotlin, Jetpack Compose, Android development, and grounded writing. The repository is also a portable Agent Plugins and the immediate skill directories under skills/.

Get the whole plugin

Other skills on chrisbanes-skills.