The easiest way to create a professional keyboard experience in Flutter.
Done · Previous / Next · Custom toolbars · Custom keyboards
Works with ListView, forms, dialogs, bottom sheets, and slivers, without scroll hacks.
Start with the example app. The example/ project is a runnable gallery with every feature. Clone the repo, run cd example && flutter run, and open each demo to see real-world usage before wiring it into your app.
| Done only | Navigation |
|---|---|
![]() |
![]() |
| Custom keyboard | Integrated bar |
|---|---|
![]() |
![]() |
| Large ListView | Dialog |
|---|---|
![]() |
![]() |
Record or refresh clips with ./tool/record_readme_gifs.sh <name> 5 while the matching example demo is open. See doc/gifs/README.md.
dependencies:
keyboard_actions: ^5.0.0Keep Flutter's default:
Scaffold(
// resizeToAvoidBottomInset: true ← leave the default
body: KeyboardActions(child: ...),
)Most keyboard packages wrap your tree in custom padding / SingleChildScrollView. That breaks nested scrollables, slivers, and sheets.
Keyboard Actions 5 does not hijack scrolling.
- Draws a floating toolbar in an
Overlay - Inflates
MediaQuery.viewInsetsby the toolbar height so a childScaffold/ Dialog / BottomSheet (resizeToAvoidBottomInset) lifts body and FAB above the Done bar - When wrapping content inside a
Scaffold, also reserves that height with real padding so aListView(which pads fromMediaQuery.padding, notviewInsets) clears the bar - Scrolls the focused field into view when it would be covered by the keyboard or bar
Scaffold(
body: KeyboardActions.done(
child: ListView(
padding: const EdgeInsets.all(20),
children: const [
TextField(decoration: InputDecoration(labelText: 'Email')),
TextField(decoration: InputDecoration(labelText: 'Password'), obscureText: true),
],
),
),
)Same widget, wrapped tighter. There is no separate API for a single field:
KeyboardActions.done(
child: TextField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(labelText: 'Phone'),
),
)Typical: wrap the Scaffold body (or a ListView / Form / single field
inside it). To also lift a FloatingActionButton above the Done bar, wrap the
Scaffold itself — including a Scaffold nested under another Scaffold.
KeyboardActions only inflates viewInsets in that case, so Scaffold resize
handles the bar once (no double gap).
KeyboardActions(
submitText: 'Submit',
onSubmit: () { /* last field */ },
child: Form(
child: ListView(
children: const [
TextField(decoration: InputDecoration(labelText: 'Name')),
TextField(decoration: InputDecoration(labelText: 'Email')),
TextField(decoration: InputDecoration(labelText: 'Notes'), maxLines: 3),
],
),
),
)No FocusNodes. No config object. Fields are discovered automatically.
Each one answers a different question, so none of them overlap:
| Fires when | Use for | |
|---|---|---|
KeyboardActions.onDismissed |
the keyboard closes, any way: Done or a dismissing tap outside | "the user finished with this field" |
KeyboardActions.onSubmit |
Submit is pressed on the last field | the form action: send, search, log in |
KeyboardField.onDone |
this field's Done is pressed | per-field cleanup; runs before onDismissed |
Submit needs onSubmit to appear, and it replaces Done on the last field.
Without it you get Done everywhere.
Use inside a [KeyboardActions] ancestor for per-field overrides. That ancestor
can wrap just this field:
KeyboardActions.done(child: KeyboardField(footer: pad, child: ...)).
Configure a field next to the widget, not in a global list. See example/lib/pages/custom_keyboard_page.dart for a full custom-keyboard setup.
KeyboardField(
toolbarButtons: [
(node) => IconButton(
icon: const Icon(Icons.keyboard_hide),
onPressed: node.unfocus,
),
],
child: const TextField(
decoration: InputDecoration(labelText: 'Email'),
),
)KeyboardField(
showBar: false, // panel has its own Done
footer: NumericKeyboard(
notifier: amount,
onDone: () => focus.unfocus(),
),
child: KeyboardCustomInput<String>(
focusNode: focus,
notifier: amount,
builder: (context, value, hasFocus) => Text(value),
),
)Per-field options: showBar, showArrows, showDone, toolbarButtons, footer / footerBuilder, onDone, optional focusNode.
All appearance lives in one object, so you style the bar once for the whole app instead of repeating parameters. See example/lib/pages/theming_page.dart, which switches presets live.
Wrap your app to style every bar in it:
KeyboardActionsTheme(
data: KeyboardActionsThemeData(
barColor: const Color(0xFF1B4D3E),
foregroundColor: Colors.white,
doneText: 'Listo',
doneTextStyle: const TextStyle(fontWeight: FontWeight.w800, fontSize: 17),
),
child: MaterialApp(...),
)It is also a ThemeExtension, so you can keep it inside your ThemeData and get light/dark handling for free:
MaterialApp(
theme: ThemeData(
extensions: const [
KeyboardActionsThemeData(barColor: Color(0xFFEEEEEE)),
],
),
)Or override a single instance:
KeyboardActions(
theme: const KeyboardActionsThemeData(barHeight: 62),
child: ...,
)Resolution order: KeyboardActions.theme → KeyboardActionsTheme → ThemeData.extensions → Material / platform defaults. Unset values fall through, so a per-screen override only replaces what it sets.
| Option | Default | Description |
|---|---|---|
barColor |
Material surface | Bar background |
foregroundColor |
colorScheme.primary |
Arrows and label color |
disabledColor |
theme.disabledColor |
Arrows at the first / last field |
barHeight |
46 |
Bar height |
elevation |
0 (M3) / 12 (M2) |
Material elevation |
borderRadius |
pill on iOS 26+ | Corner radius |
keyboardGap |
8 on iOS 26+, else 0 |
Space between bar and keyboard |
padding |
horizontal: 2 |
Padding around bar content |
doneTextStyle |
bold, foregroundColor |
Done label style |
submitTextStyle |
w600, foregroundColor |
Submit label style |
previousIcon / nextIcon |
chevrons | Custom arrow icons |
doneText |
'Done' |
Done label, set here to localize app-wide |
submitText |
'Submit' |
Submit label |
integratedBar |
false |
Flush bar against the keyboard |
Text styles are merged over the defaults, so doneTextStyle: TextStyle(fontSize: 20) changes the size and keeps the resolved color.
On iOS 26+ the bar floats above the system keyboard with a small gap and rounded corners, matching the new keyboard shape. On older iOS and Android it sits flush by default.
Set integratedBar: true for a classic accessory toolbar that is always flush with the keyboard, with no gap and no rounded corners. See example/lib/pages/integrated_bar_page.dart.
KeyboardActions(
theme: const KeyboardActionsThemeData(integratedBar: true),
child: ...,
)That is the whole public API, and the first three cover almost everything.
| API | Purpose |
|---|---|
KeyboardActions |
The wrapper: navigation, Done/Submit, reserved space, overlay bar |
KeyboardActions.done |
Shortcut: Done only, no Prev/Next |
KeyboardField |
Local per-field overrides + custom footer |
KeyboardActionsThemeData |
All bar appearance in one object |
KeyboardActionsTheme |
Applies theme data to a subtree |
KeyboardCustomInput |
Focusable value display for custom keyboards |
KeyboardCustomPanelMixin |
Helper mixin for custom panel widgets |
KeyboardNavigation.none / .auto |
Arrows off / on when 2+ fields |
Behavior lives on the widget; appearance lives in the theme.
| Option | Default | Description |
|---|---|---|
navigation |
auto |
Prev/Next arrows when 2+ fields |
ensureVisible |
true |
Scroll focused field above keyboard + bar |
ensureVisibleAlignment |
0.15 |
Target alignment when scrolling |
dismissOnTapOutside |
true |
Unfocus on a tap outside the field/bar (scrolls don't dismiss) |
doneText |
theme, then 'Done' |
Done button label for this instance |
submitText |
theme, then 'Submit' |
Submit label, replaces Done on the last field |
onSubmit |
null |
Required for Submit to appear on the last field |
onDismissed |
null |
Called whenever the keyboard closes (Done or tap outside) |
theme |
null |
Appearance overrides for this instance |
enabled |
null (iOS + Android) |
null = auto, true = always, false = never |
KeyboardActions(
navigation: KeyboardNavigation.auto,
ensureVisible: true,
dismissOnTapOutside: true,
submitText: 'Submit',
onSubmit: () {},
theme: KeyboardActionsThemeData(
barColor: Colors.grey.shade200,
doneTextStyle: const TextStyle(color: Colors.green),
),
child: ...,
)The best way to learn the package is to run the example project and tap through every screen:
git clone https://github.com/diegoveloper/flutter_keyboard_actions.git
cd flutter_keyboard_actions/example
flutter runEach demo lives under example/lib/pages/:
Custom keyboard widgets are in example/lib/widgets/custom_keyboards.dart.
| Old | New |
|---|---|
KeyboardActionsConfig + FocusNode lists |
Usually unnecessary: auto discovery |
BottomAreaAvoider / autoScroll padding |
Removed: reserved space + scroll correction |
resizeToAvoidBottomInset: false |
Keep Flutter default (true) |
KeyboardActions.simple / .auto |
KeyboardActions.done / KeyboardActions(...) |
Global actions: [KeyboardActionsItem(...)] |
Local KeyboardField(...) |
keyboardBarColor / keyboardBarElevation |
KeyboardActionsThemeData |
nextIcon / previousIcon on config |
KeyboardActionsThemeData |
MIT





