Focus management
Focus decides which part of your interface receives keyboard input. Fleury’s built-in controls can already take focus, show it, and handle the keys they understand.
Movement between controls is called focus traversal. Fleury provides two forms automatically:
- Sequential traversal: Tab and Shift+Tab follow visual reading order.
- Spatial navigation: the arrow keys move through the two-dimensional layout.
Both forms work out of the box in a FleuryApp. You work with focus directly
only when you build a custom control, move focus after an action, observe an
active region, or construct an overlay. For commands that run after a key
reaches the focused region, see
Key handling.
| You need to… | Use |
|---|---|
| Let users move between ordinary controls | Built in with FleuryApp |
| Make a custom widget focusable | Focus |
| Move focus from code | FocusNode |
| React when focus enters or leaves a subtree | FocusDetector |
Render from whether this Focus is the keyboard target | Focus.of(context).hasFocus |
| Open a dialog and restore focus afterward | context.present |
Automatic traversal
Section titled “Automatic traversal”FleuryApp gives every screen and dialog automatic focus traversal:
FleuryApp( title: 'Workspace', home: Row( children: [ Expanded( child: Column( children: [ Button(text: 'New file', autofocus: true, onPressed: createFile), Button(text: 'Open file', onPressed: openFile), Button(text: 'Settings', onPressed: openSettings), ], ), ), Expanded( child: Column( children: [ Button(text: 'Refresh', onPressed: refresh), Button(text: 'Inspect', onPressed: inspect), Button(text: 'Publish…', onPressed: publish), ], ), ), ], ),)A focused control always sees the key first. An arrow moves a list selection or text cursor while that control can use it; at the edge, the unhandled arrow can continue outward and move focus. This gives collection widgets useful internal navigation without trapping the user.
Fleury provides this policy with a FocusTraversalGroup at each route root.
You normally do not add one yourself; build the layout and let traversal follow
it.
Click the example, then try the behavior together:
- Tab scans each visual row from left to right; Shift+Tab reverses it.
- Arrow keys move to a control in the requested direction.
- Enter activates the focused button.
Publish…opens a dialog; Tab stays inside, then focus returns toPublish…when the dialog closes.
The highlighted panel only makes the automatic path visible. The screen does not configure focus traversal.
How arrow movement works
Section titled “How arrow movement works”Arrow-driven focus movement is commonly called spatial navigation. Fleury uses the painted layout and the widget tree together. When a focused control leaves an arrow unhandled, Fleury:
- Considers visible, focusable controls in the requested direction.
- Prefers controls in the same logical region, such as siblings in one pane.
- Prefers a focusable child over its focusable container.
- Chooses the best-aligned nearby control, with a stable order as the final tie-breaker.
Arrows do not wrap. If there is no target in that direction, the key remains unhandled so an ancestor can respond. Because navigation follows the rendered layout, moving or resizing a control updates the focus path automatically.
Focus targets
Section titled “Focus targets”Initial focus
Section titled “Initial focus”Use autofocus for the one control that should be ready when a screen or dialog
first appears:
TextInput( autofocus: true, placeholder: 'Search files', onChanged: filterFiles,)Autofocus is scoped and one-shot. It fills an empty focus scope when the widget appears; it does not steal focus again on every rebuild.
Custom targets
Section titled “Custom targets”Wrap a custom control in Focus when it should receive keys and participate in
traversal:
Focus( autofocus: true, child: TimelineEditor(),)Keys travel from the focused node up through its ancestors, so wrap the
Focus itself: in a KeyDetector for the control’s own key handling, or in
KeyBindings for declared commands. A detector placed inside the Focus
never sees the keys.
Programmatic focus
Section titled “Programmatic focus”Own a FocusNode when an action should move focus to a specific control. In the
demo, press Enter on Focus search, then type a query: the action moves focus
straight to the search field, without stepping through the controls between
them.
A node is an owned resource: create it once, keep it across rebuilds, and
dispose it with the state that owns it. Use hasFocus when behavior depends on
that exact target. Built-in controls accept a focusNode; wrap a custom target
in Focus(focusNode: ...) instead.
Focus changes
Section titled “Focus changes”Use FocusDetector to react when focus crosses the boundary of a whole
subtree. It is useful for active-pane styling, cursor visibility, and pausing
an inactive preview:
Tab from Title to Body: both controls remain inside the editor, so the boundary counter does not change. Tab to Preview and the detector reports one leave. This avoids false leave-and-enter cycles when focus merely moves between children of the same region.
Reading focus while you build
Section titled “Reading focus while you build”FocusDetector and Focus.of(context).hasFocus answer different questions:
FocusDetectorreports whether anything inside a region has focus. Moving between its children is not a leave, which makes it the active-pane signal. Detectors nest like CSS:focus-within: every detector around the focused widget reports focus, so aPanelstill highlights when its content uses a detector of its own.Focus.of(context).hasFocusis true only while the nearestFocusis the focused node. Inside a pane of buttons or inputs, it is false while a child holds the keyboard. Use it when the region is itself one focus stop, such as a list or a slider.
Reading Focus.of during build subscribes the widget, so it rebuilds when
that node gains or loses focus without a detector or setState.
FocusManager.of(context) returns the application’s focus manager, which
knows what has focus anywhere, and rebuilds the reader on every focus move.
KeyBindings and KeyDetector are not focus targets; Focus.of skips past
them to the enclosing Focus.
A list is one focus stop with a row cursor. highlighted means current and
stays true when the keyboard leaves; Focus.of(context).hasFocus is whether
the list itself holds the keyboard. ListView already paints the theme
selection style on the current row, so a two-state look — marker always, fill
only while focused — has to replace that default rather than merge onto
it:
ListView.builder( focusNode: listFocus, itemCount: keys.length, itemBuilder: (context, index, highlighted) { final theme = Theme.of(context); final focused = highlighted && Focus.of(context).hasFocus; return DefaultTextStyle( style: focused ? theme.textStyle.merge(theme.selectionStyle) : theme.textStyle, child: Row( children: [ Text(highlighted ? '> ' : ' '), Text(keys[index]), ], ), ); },)Overlay entries such as menus, toasts, and the error overlay sit beside the
app root rather than under it. Inside an entry with no Focus of its own,
Focus.maybeOf returns null and Focus.of throws, while FocusManager.of
still works.
Screens and dialogs
Section titled “Screens and dialogs”Each screen and dialog gets its own FocusScope. A scope remembers the last
control focused inside it, so returning to a screen restores the user’s place.
Opening another screen or dialog activates its scope; closing it restores the
previous screen’s remembered control.
context.present also traps focus inside the dialog. Tab, arrow traversal,
clicks, and direct focus requests cannot move focus to the covered screen, and
unmatched keys do not fall through to it. The Publish… flow in the first demo
shows both behaviors: focus starts on Cancel, stays in the dialog, and returns
to Publish… when the dialog closes.
final confirmed = await context.present<bool>( const ConfirmPublishDialog(),);
if (confirmed == true) publish();Pages pushed with Navigator.push get the same focus memory and restoration,
without trapping app-level focus or shortcuts as a dialog would.
Advanced patterns
Section titled “Advanced patterns”Custom focus traps
Section titled “Custom focus traps”Most applications never configure a focus trap directly. Use
FocusScope(trapFocus: true) only for a custom overlay that does not go through
context.present:
FocusScope( trapFocus: true, child: CommandPalette(),)Every FocusScope remembers its last focused descendant. trapFocus adds the
stronger rule that focus cannot leave the subtree; it does not swallow key
events. When unmatched keys must not reach the app behind the overlay either,
also wrap it in KeyBindings(modal: true). context.present applies both
for you.
Bare roots
Section titled “Bare roots”Every screen and dialog hosted by FleuryApp already gets the traversal
behavior shown above. FocusTraversalGroup is the lower-level widget that
provides that policy, not an extra boundary that ordinary panes and toolbars
need to add.
Neither runApp nor mountApp installs one. Create one directly only for a
low-level tree passed to either without FleuryApp or Navigator:
runApp(FocusTraversalGroup(child: EmbeddedSurface()));Focus exclusion
Section titled “Focus exclusion”These APIs answer different questions:
// Clickable or programmatically focusable, but skipped by traversal.final _helpFocus = FocusNode(skipTraversal: true);Button(text: 'Help', focusNode: _helpFocus, onPressed: showHelp);
// Cannot receive focus by traversal, click, or requestFocus().Focus(canRequestFocus: false, child: DecorativeCanvas());
// Remove an entire hidden or inactive subtree from focus.ExcludeFocus(child: OffstagePage());The flags belong to the node that takes focus. A built-in control owns its
node, so give it a FocusNode created with the flag, and own and dispose it
like any other node. Wrapping the control in Focus(skipTraversal: true) would
only flag the wrapper. Use the Focus flags for a custom target that has no
node of its own, and ExcludeFocus for a whole inactive page. Skipping
traversal does not disable pointer input.
API reference
Section titled “API reference”| API | What it does |
|---|---|
Focus | Makes a custom subtree focusable. Focus.of returns its node. |
FocusNode | Owns a target you can request, inspect, and dispose. |
FocusDetector | Reports focus entering or leaving a subtree. |
FocusManager.of | The application’s focus manager. |
FocusScope | Remembers focus within a subtree; trapFocus can contain it. |
FocusTraversalGroup | Installs Tab and arrow traversal for a bare root. |
ExcludeFocus | Removes a whole subtree from every focus path. |
Next steps
Section titled “Next steps”- Add commands with Key handling.
- Move focus after validation in Forms & validation.
- Learn how collections consume and release arrows in Lists & scrolling.
- API references:
Focus,FocusNode,FocusDetector, and theFocusTraversalGroupsource.