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'),
)

Focus best practices

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

Keep focus visible

Don't let focused items scroll off-screen. Scroll to keep them visible.

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!

Common patterns

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