Skip to content

Focus

Understand how to manage keyboard focus in Flutter applications.


What is it?

Focus is the system that manages which widget is currently receiving keyboard input in a Flutter application. When a TextField or other input widget has focus, it means the user can type into it. Focus management allows you to control where the keyboard appears, navigate between fields, and handle focus changes programmatically.


Why does it exist?

Focus exists to:

  • Manage keyboard input focus
  • Enable keyboard navigation
  • Control input fields programmatically
  • Handle focus transitions
  • Support accessibility
  • Improve user experience
  • Manage form navigation

Basic Focus Management

Managing focus with FocusNode.

// Import required packages
import 'package:flutter/material.dart';

/// Basic focus management example
class BasicFocusExample extends StatefulWidget {
  const BasicFocusExample({super.key});

  @override
  State<BasicFocusExample> createState() => _BasicFocusExampleState();
}

class _BasicFocusExampleState extends State<BasicFocusExample> {
  // 1. Create FocusNodes for each field
  // FocusNode tracks focus state for a specific widget
  final FocusNode _nameFocus = FocusNode();
  final FocusNode _emailFocus = FocusNode();
  final FocusNode _passwordFocus = FocusNode();

  // Controllers for fields
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  void initState() {
    super.initState();

    // 2. Add listeners to focus nodes
    // This is useful for reacting to focus changes
    _nameFocus.addListener(() {
      // Check if this field gained focus
      if (_nameFocus.hasFocus) {
        print('Name field focused');
      } else {
        print('Name field unfocused');
      }
    });
  }

  @override
  void dispose() {
    // 3. Always dispose focus nodes
    // This prevents memory leaks
    _nameFocus.dispose();
    _emailFocus.dispose();
    _passwordFocus.dispose();

    _nameController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Focus Management'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 4. Name field with focus node
            // The focus node controls focus for this field
            TextField(
              controller: _nameController,
              focusNode: _nameFocus,
              decoration: const InputDecoration(
                labelText: 'Name',
                hintText: 'Enter your name',
                border: OutlineInputBorder(),
              ),
              // 5. Handle submit action
              // Called when user presses "Next" or "Done"
              onSubmitted: (value) {
                // Move focus to next field
                // This shows how to programmatically move focus
                _emailFocus.requestFocus();
              },
            ),
            const SizedBox(height: 16),

            // 6. Email field
            TextField(
              controller: _emailController,
              focusNode: _emailFocus,
              decoration: const InputDecoration(
                labelText: 'Email',
                hintText: 'Enter your email',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.emailAddress,
              onSubmitted: (value) {
                // Move focus to password field
                _passwordFocus.requestFocus();
              },
            ),
            const SizedBox(height: 16),

            // 7. Password field
            TextField(
              controller: _passwordController,
              focusNode: _passwordFocus,
              decoration: const InputDecoration(
                labelText: 'Password',
                hintText: 'Enter your password',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
              onSubmitted: (value) {
                // Submit the form
                // Unfocus to dismiss keyboard
                _passwordFocus.unfocus();
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text('Form submitted!'),
                    backgroundColor: Colors.green,
                  ),
                );
              },
            ),

            const SizedBox(height: 24),

            // 8. Focus control buttons
            // These demonstrate programmatic focus control
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // Request focus on name field
                ElevatedButton(
                  onPressed: () {
                    // requestFocus() moves focus to the field
                    _nameFocus.requestFocus();
                  },
                  child: const Text('Focus Name'),
                ),
                const SizedBox(width: 8),

                // Request focus on email field
                ElevatedButton(
                  onPressed: () {
                    _emailFocus.requestFocus();
                  },
                  child: const Text('Focus Email'),
                ),
                const SizedBox(width: 8),

                // Unfocus all fields (dismiss keyboard)
                ElevatedButton(
                  onPressed: () {
                    // unfocus() removes focus from all fields
                    // This dismisses the keyboard
                    FocusScope.of(context).unfocus();
                  },
                  child: const Text('Dismiss Keyboard'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - FocusNode manages focus for a widget - requestFocus() moves focus to a field - unfocus() dismisses the keyboard - FocusScope manages focus scope - onSubmitted handles action


FocusScope

Managing groups of focus nodes.

/// FocusScope example
class FocusScopeExample extends StatefulWidget {
  const FocusScopeExample({super.key});

  @override
  State<FocusScopeExample> createState() => _FocusScopeExampleState();
}

class _FocusScopeExampleState extends State<FocusScopeExample> {
  // Focus nodes for each field
  final FocusNode _field1Focus = FocusNode();
  final FocusNode _field2Focus = FocusNode();
  final FocusNode _field3Focus = FocusNode();

  // FocusScope node for the form
  // This manages focus for all fields in the scope
  final FocusScopeNode _formScope = FocusScopeNode();

  @override
  void dispose() {
    _field1Focus.dispose();
    _field2Focus.dispose();
    _field3Focus.dispose();
    _formScope.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FocusScope Example'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: FocusScope(
          // 1. Assign the scope node to the FocusScope
          // This groups all child focus nodes
          node: _formScope,

          // 2. Child widgets
          child: Column(
            children: [
              // Field 1
              TextField(
                focusNode: _field1Focus,
                decoration: const InputDecoration(
                  labelText: 'Field 1',
                  border: OutlineInputBorder(),
                ),
                onSubmitted: (_) {
                  // 3. Move focus to next field in scope
                  // This respects the focus order
                  _formScope.nextFocus();
                },
              ),
              const SizedBox(height: 16),

              // Field 2
              TextField(
                focusNode: _field2Focus,
                decoration: const InputDecoration(
                  labelText: 'Field 2',
                  border: OutlineInputBorder(),
                ),
                onSubmitted: (_) {
                  // Move to next field
                  _formScope.nextFocus();
                },
              ),
              const SizedBox(height: 16),

              // Field 3
              TextField(
                focusNode: _field3Focus,
                decoration: const InputDecoration(
                  labelText: 'Field 3',
                  border: OutlineInputBorder(),
                ),
                onSubmitted: (_) {
                  // If last field, unfocus
                  _formScope.unfocus();
                },
              ),

              const SizedBox(height: 24),

              // FocusScope control buttons
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // Focus first field
                  ElevatedButton(
                    onPressed: () {
                      _formScope.requestFocus(_field1Focus);
                    },
                    child: const Text('First Field'),
                  ),
                  const SizedBox(width: 8),

                  // Focus next field
                  ElevatedButton(
                    onPressed: () {
                      // Moves focus to the next field in scope
                      _formScope.nextFocus();
                    },
                    child: const Text('Next Field'),
                  ),
                  const SizedBox(width: 8),

                  // Unfocus all
                  ElevatedButton(
                    onPressed: () {
                      // Removes focus from all fields in scope
                      _formScope.unfocus();
                    },
                    child: const Text('Unfocus'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

What's happening here? - FocusScope groups focus nodes - nextFocus() moves to next field - requestFocus() focuses a specific field - unfocus() removes focus from all


Focus Listeners

Reacting to focus changes.

/// Focus listeners example
class FocusListenersExample extends StatefulWidget {
  const FocusListenersExample({super.key});

  @override
  State<FocusListenersExample> createState() => _FocusListenersExampleState();
}

class _FocusListenersExampleState extends State<FocusListenersExample> {
  // Focus nodes
  final FocusNode _field1Focus = FocusNode();
  final FocusNode _field2Focus = FocusNode();

  // State for focus tracking
  bool _field1HasFocus = false;
  bool _field2HasFocus = false;

  @override
  void initState() {
    super.initState();

    // 1. Add listeners to focus nodes
    // These listeners are called when focus changes
    _field1Focus.addListener(() {
      // Update state when focus changes
      setState(() {
        _field1HasFocus = _field1Focus.hasFocus;
      });
    });

    _field2Focus.addListener(() {
      setState(() {
        _field2HasFocus = _field2Focus.hasFocus;
      });
    });
  }

  @override
  void dispose() {
    _field1Focus.dispose();
    _field2Focus.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Focus Listeners'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 2. Field 1 with focus listener
            Container(
              decoration: BoxDecoration(
                // Highlight when focused
                border: Border.all(
                  color: _field1HasFocus ? Colors.blue : Colors.transparent,
                  width: 2,
                ),
                borderRadius: BorderRadius.circular(8),
              ),
              child: TextField(
                focusNode: _field1Focus,
                decoration: const InputDecoration(
                  labelText: 'Field 1',
                  hintText: 'Click to focus',
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            const SizedBox(height: 16),

            // 3. Field 2 with focus listener
            Container(
              decoration: BoxDecoration(
                border: Border.all(
                  color: _field2HasFocus ? Colors.blue : Colors.transparent,
                  width: 2,
                ),
                borderRadius: BorderRadius.circular(8),
              ),
              child: TextField(
                focusNode: _field2Focus,
                decoration: const InputDecoration(
                  labelText: 'Field 2',
                  hintText: 'Click to focus',
                  border: OutlineInputBorder(),
                ),
              ),
            ),

            const SizedBox(height: 24),

            // 4. Focus status display
            Card(
              color: Colors.blue[50],
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  children: [
                    Text(
                      'Field 1: ${_field1HasFocus ? "Focused ✓" : "Not Focused"}',
                      style: TextStyle(
                        color: _field1HasFocus ? Colors.blue : Colors.grey,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      'Field 2: ${_field2HasFocus ? "Focused ✓" : "Not Focused"}',
                      style: TextStyle(
                        color: _field2HasFocus ? Colors.blue : Colors.grey,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - addListener tracks focus changes - hasFocus checks current focus state - Visual feedback on focus - Real-time focus status display


Real-World Examples

Common focus management patterns.

/// 1. Login form with focus management
class LoginFormFocus extends StatefulWidget {
  const LoginFormFocus({super.key});

  @override
  State<LoginFormFocus> createState() => _LoginFormFocusState();
}

class _LoginFormFocusState extends State<LoginFormFocus> {
  // Focus nodes for form fields
  final FocusNode _usernameFocus = FocusNode();
  final FocusNode _passwordFocus = FocusNode();

  // Form controllers
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  // Form key
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  // Track focus states for styling
  bool _usernameHasFocus = false;
  bool _passwordHasFocus = false;

  @override
  void initState() {
    super.initState();

    // Add focus listeners
    _usernameFocus.addListener(() {
      setState(() {
        _usernameHasFocus = _usernameFocus.hasFocus;
      });
    });

    _passwordFocus.addListener(() {
      setState(() {
        _passwordHasFocus = _passwordFocus.hasFocus;
      });
    });
  }

  @override
  void dispose() {
    _usernameFocus.dispose();
    _passwordFocus.dispose();
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      // Form is valid
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Login successful!'),
          backgroundColor: Colors.green,
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Login Form'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // Username field
              TextFormField(
                controller: _usernameController,
                focusNode: _usernameFocus,
                decoration: InputDecoration(
                  labelText: 'Username',
                  hintText: 'Enter your username',
                  prefixIcon: const Icon(Icons.person),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                  // Change border color when focused
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                    borderSide: BorderSide(
                      color: _usernameHasFocus ? Colors.blue : Colors.grey,
                      width: 2,
                    ),
                  ),
                ),
                // Move to password field on submit
                onFieldSubmitted: (_) {
                  _passwordFocus.requestFocus();
                },
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your username';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),

              // Password field
              TextFormField(
                controller: _passwordController,
                focusNode: _passwordFocus,
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'Password',
                  hintText: 'Enter your password',
                  prefixIcon: const Icon(Icons.lock),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                    borderSide: BorderSide(
                      color: _passwordHasFocus ? Colors.blue : Colors.grey,
                      width: 2,
                    ),
                  ),
                ),
                onFieldSubmitted: (_) {
                  // Submit form when done is pressed
                  _submitForm();
                },
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your password';
                  }
                  if (value.length < 6) {
                    return 'Password must be at least 6 characters';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),

              // Login button
              SizedBox(
                width: double.infinity,
                height: 50,
                child: ElevatedButton(
                  onPressed: _submitForm,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                  ),
                  child: const Text(
                    'Login',
                    style: TextStyle(fontSize: 16),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// 2. Autofocus and focus traversal
class FocusTraversalExample extends StatefulWidget {
  const FocusTraversalExample({super.key});

  @override
  State<FocusTraversalExample> createState() => _FocusTraversalExampleState();
}

class _FocusTraversalExampleState extends State<FocusTraversalExample> {
  // Focus nodes for a grid of fields
  final List<List<FocusNode>> _focusNodes = [];
  final List<List<TextEditingController>> _controllers = [];

  @override
  void initState() {
    super.initState();

    // Initialize focus nodes and controllers for a 3x3 grid
    for (int i = 0; i < 3; i++) {
      final rowNodes = <FocusNode>[];
      final rowControllers = <TextEditingController>[];
      for (int j = 0; j < 3; j++) {
        rowNodes.add(FocusNode());
        rowControllers.add(TextEditingController());
      }
      _focusNodes.add(rowNodes);
      _controllers.add(rowControllers);
    }
  }

  @override
  void dispose() {
    // Dispose all focus nodes and controllers
    for (final row in _focusNodes) {
      for (final node in row) {
        node.dispose();
      }
    }
    for (final row in _controllers) {
      for (final controller in row) {
        controller.dispose();
      }
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Focus Traversal'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // Grid of fields
            ...List.generate(3, (row) {
              return Row(
                children: List.generate(3, (col) {
                  return Expanded(
                    child: Padding(
                      padding: const EdgeInsets.all(4),
                      child: TextField(
                        controller: _controllers[row][col],
                        focusNode: _focusNodes[row][col],
                        decoration: InputDecoration(
                          labelText: '$row,$col',
                          border: const OutlineInputBorder(),
                        ),
                        // Move focus to next field in row
                        onEditingComplete: () {
                          // Move to next field in same row
                          if (col < 2) {
                            _focusNodes[row][col + 1].requestFocus();
                          } else if (row < 2) {
                            // Move to first field in next row
                            _focusNodes[row + 1][0].requestFocus();
                          } else {
                            // Last field - unfocus
                            _focusNodes[row][col].unfocus();
                          }
                        },
                      ),
                    ),
                  );
                }),
              );
            }),

            const SizedBox(height: 24),

            // Navigation controls
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // Focus first field
                ElevatedButton(
                  onPressed: () {
                    _focusNodes[0][0].requestFocus();
                  },
                  child: const Text('First Field'),
                ),
                const SizedBox(width: 8),

                // Focus last field
                ElevatedButton(
                  onPressed: () {
                    _focusNodes[2][2].requestFocus();
                  },
                  child: const Text('Last Field'),
                ),
                const SizedBox(width: 8),

                // Unfocus all
                ElevatedButton(
                  onPressed: () {
                    FocusScope.of(context).unfocus();
                  },
                  child: const Text('Unfocus'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - Login form with focus navigation - Visual focus indicators - Grid focus traversal - Autofocus and focus management


Best Practices

Dispose Focus Nodes

// Good - Dispose in dispose()
@override
void dispose() {
  _focusNode.dispose();
  super.dispose();
}

// Bad - Not disposing
@override
void dispose() {
  // Missing focus node dispose
  super.dispose();
}

Use FocusScope for Groups

// Good - Group fields in FocusScope
FocusScope(
  node: _formScope,
  child: Column(
    children: formFields,
  ),
)

// Bad - Individual focus management
// Each field manages its own focus independently

Handle Focus Changes

// Good - React to focus changes
_focusNode.addListener(() {
  if (_focusNode.hasFocus) {
    // Handle focus gained
  } else {
    // Handle focus lost
  }
});

Common Mistakes

Not Disposing Focus Nodes

Wrong:

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final FocusNode _focusNode = FocusNode();
  // No dispose - memory leak
}

Correct:

class _MyWidgetState extends State<MyWidget> {
  final FocusNode _focusNode = FocusNode();

  @override
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }
}

Not Using requestFocus

Wrong:

// Manually managing focus
_focusNode.hasFocus = true; // Doesn't work

Correct:

// Using requestFocus
_focusNode.requestFocus();


Summary

Focus management controls keyboard input focus in Flutter applications. Use FocusNode for individual fields, FocusScope for groups, and focus listeners for reacting to focus changes. Always dispose focus nodes to prevent memory leaks.


Next Steps


Did You Know?

  • FocusNode must be disposed
  • FocusScope groups focus nodes
  • requestFocus() moves focus
  • nextFocus() moves to next field
  • Focus listeners track changes
  • Autofocus sets initial focus
  • Focus determines keyboard visibility
  • Focus enables keyboard navigation