Keyboard Actions 5

August 10, 2026 · View on GitHub

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.

Preview

Done onlyNavigation
Done only toolbarPrev / Next navigation
Custom keyboardIntegrated bar
Custom keyboard panelIntegrated bar
Large ListViewDialog
Large ListView scrollDialog keyboard

Record or refresh clips with ./tool/record_readme_gifs.sh <name> 5 while the matching example demo is open. See doc/gifs/README.md.


Install

dependencies:
  keyboard_actions: ^5.0.0

Keep Flutter's default:

Scaffold(
  // resizeToAvoidBottomInset: true  ← leave the default
  body: KeyboardActions(child: ...),
)

How it avoids the usual keyboard bugs

Most keyboard packages wrap your tree in custom padding / SingleChildScrollView. That breaks nested scrollables, slivers, and sheets.

Keyboard Actions 5 does not hijack scrolling.

  1. Draws a floating toolbar in an Overlay
  2. Inflates MediaQuery.viewInsets by the toolbar height so a child Scaffold / Dialog / BottomSheet (resizeToAvoidBottomInset) lifts body and FAB above the Done bar
  3. When wrapping content inside a Scaffold, also reserves that height with real padding so a ListView (which pads from MediaQuery.padding, not viewInsets) clears the bar
  4. Scrolls the focused field into view when it would be covered by the keyboard or bar

Quick start

Done only

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

One field only

Same widget, wrapped tighter. There is no separate API for a single field:

KeyboardActions.done(
  child: TextField(
    keyboardType: TextInputType.phone,
    decoration: InputDecoration(labelText: 'Phone'),
  ),
)

Where to wrap

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).


Prev / Next / Done (default)

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.

The three callbacks

Each one answers a different question, so none of them overlap:

Fires whenUse for
KeyboardActions.onDismissedthe keyboard closes, any way: Done or a dismissing tap outside"the user finished with this field"
KeyboardActions.onSubmitSubmit is pressed on the last fieldthe form action: send, search, log in
KeyboardField.onDonethis field's Done is pressedper-field cleanup; runs before onDismissed

Submit needs onSubmit to appear, and it replaces Done on the last field. Without it you get Done everywhere.


Per-field customization: KeyboardField

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

Custom keyboard panel

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.


Toolbar styling: KeyboardActionsThemeData

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.themeKeyboardActionsThemeThemeData.extensions → Material / platform defaults. Unset values fall through, so a per-screen override only replaces what it sets.

Theme options

OptionDefaultDescription
barColorMaterial surfaceBar background
foregroundColorcolorScheme.primaryArrows and label color
disabledColortheme.disabledColorArrows at the first / last field
barHeight46Bar height
elevation0 (M3) / 12 (M2)Material elevation
borderRadiuspill on iOS 26+Corner radius
keyboardGap8 on iOS 26+, else 0Space between bar and keyboard
paddinghorizontal: 2Padding around bar content
doneTextStylebold, foregroundColorDone label style
submitTextStylew600, foregroundColorSubmit label style
previousIcon / nextIconchevronsCustom arrow icons
doneText'Done'Done label, set here to localize app-wide
submitText'Submit'Submit label
integratedBarfalseFlush bar against the keyboard

Text styles are merged over the defaults, so doneTextStyle: TextStyle(fontSize: 20) changes the size and keeps the resolved color.

iOS 26 and integratedBar

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: ...,
)

API overview

That is the whole public API, and the first three cover almost everything.

APIPurpose
KeyboardActionsThe wrapper: navigation, Done/Submit, reserved space, overlay bar
KeyboardActions.doneShortcut: Done only, no Prev/Next
KeyboardFieldLocal per-field overrides + custom footer
KeyboardActionsThemeDataAll bar appearance in one object
KeyboardActionsThemeApplies theme data to a subtree
KeyboardCustomInputFocusable value display for custom keyboards
KeyboardCustomPanelMixinHelper mixin for custom panel widgets
KeyboardNavigation.none / .autoArrows off / on when 2+ fields

KeyboardActions options

Behavior lives on the widget; appearance lives in the theme.

OptionDefaultDescription
navigationautoPrev/Next arrows when 2+ fields
ensureVisibletrueScroll focused field above keyboard + bar
ensureVisibleAlignment0.15Target alignment when scrolling
dismissOnTapOutsidetrueUnfocus on a tap outside the field/bar (scrolls don't dismiss)
doneTexttheme, then 'Done'Done button label for this instance
submitTexttheme, then 'Submit'Submit label, replaces Done on the last field
onSubmitnullRequired for Submit to appear on the last field
onDismissednullCalled whenever the keyboard closes (Done or tap outside)
themenullAppearance overrides for this instance
enablednull (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 run

Each demo lives under example/lib/pages/:

DemoWhat it showsPreview
Done onlyZero config login-style formDone only
Form + navigationPrev / Next / Done / SubmitNavigation
ThemingColors, height, labels and icons via theme
Integrated barintegratedBar: true, flush toolbarIntegrated bar
Material 2Classic M2 theme + integrated bar
Large ListView40+ fields, no scroll wrapper hacksLarge ListView
Custom keyboardsCounter, color picker, numeric panelsCustom keyboard
DialogWorks inside AlertDialogDialog
Bottom sheetCheckout-style modal sheet
Nested scrollCustomScrollView + slivers

Custom keyboard widgets are in example/lib/widgets/custom_keyboards.dart.


Migration from 4.x

OldNew
KeyboardActionsConfig + FocusNode listsUsually unnecessary: auto discovery
BottomAreaAvoider / autoScroll paddingRemoved: reserved space + scroll correction
resizeToAvoidBottomInset: falseKeep Flutter default (true)
KeyboardActions.simple / .autoKeyboardActions.done / KeyboardActions(...)
Global actions: [KeyboardActionsItem(...)]Local KeyboardField(...)
keyboardBarColor / keyboardBarElevationKeyboardActionsThemeData
nextIcon / previousIcon on configKeyboardActionsThemeData

License

MIT