How to Show a “Done” Button to Dismiss the Keyboard in iOS Using Flutter (A Simple, Reusable Approach)

Published: (January 13, 2026 at 04:31 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

When building text‑intensive screens in Flutter, one of the most overlooked UX details is how users dismiss the iOS keyboard, especially with multiline or numeric input fields. Android keyboards typically provide a visible “Done” or “Close” action, but iOS often omits this, leaving users stuck unless they tap outside the input or scroll awkwardly.

Adding a custom “Done” button overlay above the iOS keyboard is straightforward. Below we explore why this feature matters, how it improves text‑input UX, and how to implement a clean, reusable solution using a Flutter mixin.

Why Does iOS Need a Custom “Done” Button?

  • Multiline fields do not show a keyboard dismissal button.
  • Numeric keyboards do not include a return/done key.
  • Bottom sheets often get blocked by the keyboard.
  • Tapping outside to dismiss is not always obvious or possible.
  • Keyboard overlays can hide critical CTA buttons.

These issues create UX friction on content‑creation screens, forms, note‑taking interfaces, or anywhere users type more than a few words. A custom overlay places a clear, intuitive Done button right above the keyboard, allowing the user to dismiss it instantly.

Benefits of Using a Mixin

  • No boilerplate duplication.
  • Keeps your widget tree clean.
  • Reusable across multiple pages.
  • Automatically manages overlay insertion and removal.
  • Fully iOS‑safe (runs only when Platform.isIOS is true).

Applying the Mixin

Below is a minimal example showing how to integrate the mixin into a page. The mixin handles focus changes and overlay management automatically.

// Example: TextInputPage with a reusable Done‑button mixin
class TextInputPage extends StatefulWidget {
  @override
  _TextInputPageState createState() => _TextInputPageState();
}

class _TextInputPageState extends State
    with KeyboardDoneButtonMixin { // <-- your mixin
  final TextEditingController _articleController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Article Editor')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: TextField(
          controller: _articleController,
          focusNode: focusNode, // provided by the mixin
          maxLines: null,
          decoration: const InputDecoration(
            hintText: 'Start typing...',
          ),
        ),
      ),
    );
  }
}

Overlay Behavior

  • Overlay Activation – When the text field gains focus, the mixin inserts an OverlayEntry positioned just above the keyboard.
  • Dynamic PositioningMediaQuery.of(context).viewInsets.bottom provides the keyboard height in real‑time, ensuring perfect alignment.
  • Safe Removal – The overlay is dismissed cleanly when focus is lost or when the user taps the Done button.
  • iOS‑only – The overlay never appears on Android, Web, or desktop platforms.

Common Use Cases

  • Chat applications.
  • Form‑driven workflows.
  • Note‑taking or content‑creation screens.

Extending the Experience

Because the overlay is a regular widget, you can:

  • Apply custom styling to match your app’s theme.
  • Animate the overlay’s entrance and exit for a polished feel.

A small detail like a Done button above the keyboard can dramatically improve the text‑input experience on iOS, especially in content‑creation or form‑driven workflows. The mixin approach keeps your codebase clean, scalable, and maintainable while delivering a native‑feeling UX.

Back to Blog

Related posts

Read more »