Compose Focus Navigation
Communitychrisbanes
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.
Overview
- Repository
- chrisbanes/skills
- Publisher
- chrisbanes
- Stars
- 1.2K
- Forks
- 58
- Bundled files
- 0
- License
- Apache-2.0
- Updated
- Nov 14, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/chrisbanes/skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/compose-focus-navigation" ~/.claude/skills/compose-focus-navigation
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/chrisbanes/skills.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skills/compose-focus-navigation" ~/.codex/skills/compose-focus-navigation
rm -rf "$tmp"Install the Compose Focus Navigation agent skill from https://github.com/chrisbanes/skills/tree/main/skills/compose-focus-navigation into this project's skills folder.Instructions
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
- 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 |
- 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. InsideAnimatedContent, 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. In a review, a finding aboutrequestFocus()running in the composable body is incomplete until it names the event or keyed effect that should issue the request after its target is available. - Keep default spatial search unless a concrete edge, jump, or trap is wrong. Encode only those exceptions with
focusProperties. - 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.
- Restore by semantic identity after refresh: retain the focused id when it exists, otherwise choose a deterministic fallback.
- 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.
- 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.
FAQ
What does Compose Focus Navigation do?
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.
Who made Compose Focus Navigation?
chrisbanes (a community member) publishes it in the chrisbanes/skills repository on GitHub.
Is Compose Focus Navigation free?
Yes. Compose Focus Navigation is open source, published by chrisbanes under the Apache-2.0 license.
What comes with Compose Focus Navigation?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use Compose Focus Navigation?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.