Focus Management
Managing focus in your application
Focus determines which component receives keyboard input. This guide explains how to manage focus in your app.
Focus system
Only one component can have focus at a time. The focused component receives all keyboard events.
Focusable(
focused: true, // This component has focus
onKeyEvent: (event) {
// Receives keyboard events
return true;
},
child: Text('Focused'),
)
Managing focus state
Track which component is focused:
class App extends StatefulComponent {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
int _focusedIndex = 0;
@override
Component build(BuildContext context) {
return Column(
children: [
Focusable(
focused: _focusedIndex == 0,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
setState(() => _focusedIndex = 1);
return true;
}
return false;
},
child: Text('Item 1'),
),
Focusable(
focused: _focusedIndex == 1,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
setState(() => _focusedIndex = 0);
return true;
}
return false;
},
child: Text('Item 2'),
),
],
);
}
}
Focus traversal
Implement Tab navigation:
void _nextFocus() {
setState(() {
_focusedIndex = (_focusedIndex + 1) % itemCount;
});
}
void _previousFocus() {
setState(() {
_focusedIndex = (_focusedIndex - 1 + itemCount) % itemCount;
});
}
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
if (event.isShiftPressed) {
_previousFocus();
} else {
_nextFocus();
}
return true;
}
return false;
}
Visual focus indicators
Show users which component has focus:
Container(
decoration: BoxDecoration(
border: BoxBorder.all(
color: isFocused ? Colors.blue : Colors.gray,
),
),
child: Text(isFocused ? '> Focused' : ' Not focused'),
)
Always show focus
Make focus visible:
// Good - clear visual indicator
Text(isFocused ? '→ Selected' : ' Item')
// Bad - no visual difference
Text('Item')
Provide Tab navigation
Users expect Tab to move focus:
if (event.logicalKey == LogicalKey.tab) {
moveFocusForward();
return true;
}
Support Shift+Tab
Go backwards with Shift+Tab:
if (event.logicalKey == LogicalKey.tab) {
if (event.isShiftPressed) {
moveFocusBackward();
} else {
moveFocusForward();
}
return true;
}
One focus at a time
Only one component should have focused: true:
// Good
Focusable(focused: index == focusedIndex, ...)
// Bad - multiple focused components
Focusable(focused: true, ...)
Focusable(focused: true, ...) // Wrong!
List with focus
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final isFocused = index == focusedIndex;
return Focusable(
focused: isFocused,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.arrowDown) {
setState(() => focusedIndex = (index + 1).clamp(0, items.length - 1));
return true;
}
if (event.logicalKey == LogicalKey.arrowUp) {
setState(() => focusedIndex = (index - 1).clamp(0, items.length - 1));
return true;
}
return false;
},
child: Text(isFocused ? '→ ${items[index]}' : ' ${items[index]}'),
);
},
)
Form focus
class FormState extends State<MyForm> {
int _focusedField = 0;
final int _fieldCount = 3;
void _nextField() {
setState(() {
_focusedField = (_focusedField + 1) % _fieldCount;
});
}
@override
Component build(BuildContext context) {
return Column(
children: [
for (var i = 0; i < _fieldCount; i++)
Focusable(
focused: _focusedField == i,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
_nextField();
return true;
}
return false;
},
child: TextField(
decoration: BoxDecoration(
border: _focusedField == i
? BoxBorder.all(color: Colors.blue)
: BoxBorder.all(color: Colors.gray),
),
),
),
],
);
}
}
Next steps
- Keyboard Events - Handle keyboard input
- Input - Input components
- Testing - Test your app