Skip to content
Tech Stack16 October 2026 · 15 min read

Fix FlutterFlow Forms Hidden by the Keyboard

The submit button sits behind the keyboard and the sheet refuses to scroll. One symptom, four separate responsibilities, and the fix is rarely the one you reach for first.

Fix FlutterFlow Forms Hidden by the Keyboard

The sheet opens, the fields accept text, and the submit button is somewhere behind the keyboard. Scrolling does nothing. On a short screen it can come down to two visible fields, a drag handle, and no way to reach the end of the form.

The short answer: a bottom sheet in FlutterFlow does not inherit the page's keyboard behavior, because the sheet is pushed as its own route rather than placed in the page body that Flutter's Scaffold resizes. Make the sheet content scrollable, bound the sheet height to the space the keyboard leaves, and pad the bottom of that scrollable by the current bottom inset. Those are three separate settings, and fixing the wrong one is why a fixed pixel height appears to work on one device and fails on the next.

Four responsibilities, one symptom

"The keyboard covers my button" describes a symptom that four different mechanisms can produce. Before changing a dimension, decide which one is failing on the screen in front of you.

ResponsibilityWhat it controlsFailure you see
Keyboard insetHow much of the bottom of the window the keyboard occupiesThe last control sits under the keyboard and nothing moves it
Scrollable contentWhether content taller than the sheet can be reachedContent is cut off and the sheet refuses to scroll
Focus movementWhether the field you just tapped ends up visibleYou type into a field you cannot see
Sheet heightHow much of the window the sheet is allowed to takeThe sheet stays short while empty space sits above it

A sheet can scroll perfectly and still hide the submit button, because scrolling to the bottom of the content only reaches the top edge of the keyboard if something reserved that much space. The two fixes live in different places.

Reproduce it with a form you control

Diagnosing this on your real form means reading three or four interacting settings at once. Build a throwaway sheet first:

  1. Create a bottom sheet component with a scrollable Column containing six text fields and a submit button at the bottom. Six is enough to exceed a short screen without the keyboard open.
  2. Set the Column's scroll behavior to Allow Scrolling. In the generated Flutter code this wraps the Row or Column in a SingleChildScrollView(), which is documented on FlutterFlow's Column page.
  3. Open the sheet on a keyboard-capable target, not the builder canvas. Tap the first field, then the last field, and try to reach the submit button.
  4. Repeat with the Column set to Do Not Allow Scrolling. The same page describes that setting as forcing "the content to fit within the available visible space, hiding overflow content or potentially causing layout issues".
  5. Repeat at the shortest width you support, then in landscape.

Run the same five steps on your real sheet afterwards. If the throwaway form behaves and yours does not, the difference is in your layout rather than in the sheet mechanism.

Why does the bottom sheet behave differently from the page?

A page body resizes itself around the keyboard automatically and a modal sheet does not, because the resize behavior belongs to Scaffold and the sheet is not inside it. Scaffold.resizeToAvoidBottomInset defaults to true, and the documentation describes its scope precisely: "the body and the scaffold's floating widgets should size themselves to avoid the onscreen keyboard", taking the keyboard height from the ambient MediaQuery.

A modal sheet is not the body. showModalBottomSheet looks up the Navigator from the context you pass it and pushes a ModalBottomSheetRoute, and its API page notes that the context "is only used when the method is called" and that the widget which supplied it "can be safely removed from the tree before the bottom sheet is closed". The sheet is a route sitting above your page, so the property that resizes the page body has nothing to say about it. Everything the sheet does around the keyboard, it does because its own content asked for it.

Where the keyboard height comes from

The keyboard is a bottom view inset. Flutter's viewInsets documentation describes these as the regions the operating system may cover with system UI "such as the keyboard, that fully obscures any content", and it is explicit about how to read the value: go through MediaQuery rather than the view directly, because that "will automatically cause any widgets dependent on the insets to rebuild when they change". The same page notes that for Material apps, Scaffold applies the view insets for you, which is exactly the help a sheet route does not get.

Inside the sheet builder, MediaQuery.of(context).viewInsets.bottom is the number of logical pixels the keyboard currently claims, and it changes while the keyboard animates. Reading it through MediaQuery is what makes the sheet follow that animation rather than jump once at the end.

Three measurements on the bottom edge of one phone screen: view padding unchanged by the keyboard, view insets as the keyboard band, and padding as the subtraction that reaches zero while the keyboard is up.

Why does a pinned submit button jump when the keyboard opens?

Because the padding a SafeArea uses is already reduced by the keyboard. Flutter computes the padding of a view as max(0.0, FlutterView.viewPadding - FlutterView.viewInsets), and the documentation for that property states the consequence directly: "This will treat a system IME that increases the bottom inset as consuming that much of the bottom padding." Its iPhone X example says the bottom value matches the view padding when the soft keyboard is not drawn "but will be 0.0 when the soft keyboard is visible".

viewPadding behaves the other way. Its own page says the value "does not change relative to" the view insets, and that on an iPhone X it "will not change in response to the soft keyboard being visible or hidden".

That gives you two deliberate choices for a footer inside a sheet:

  • SafeArea insets by padding, so the bottom gesture gap disappears the moment the keyboard opens and returns when it closes. The SafeArea documentation adds that the child's MediaQuery padding is set to zero on every side the SafeArea consumed, so a nested SafeArea will not put it back.
  • SafeArea(maintainBottomViewPadding: true, ...) keeps the gap. That parameter defaults to false, and when true it uses the bottom viewPadding instead of the bottom padding.

Neither choice lifts anything above the keyboard. Clearance for the keyboard itself is the inset, and you add that yourself.

What actually scrolls the focused field into view

A text field moves itself. TextField.scrollPadding is documented as configuring "the padding for the edges surrounding a Scrollable when the text field scrolls into view", defaulting to EdgeInsets.all(20.0), and the behavior it describes is conditional: a field that receives focus while not completely visible "will attempt to make itself visible by scrolling a surrounding Scrollable".

That last condition is the whole problem. With no surrounding scrollable there is nothing for the field to scroll inside, which is why a sheet whose Column is set to Do Not Allow Scrolling leaves you typing into a field you cannot see. For an invisible focused field, the change that usually helps is making the content scrollable rather than adding a focus or scroll action.

FlutterFlow's scroll actions do not substitute for this. The Scroll To action moves "a scrollable widget to its beginning or end" and names ListView, StaggeredView and GridView as its targets, with Beginning or End and a duration in milliseconds. Scroll To Index targets a list item by index. Neither is documented for scrolling to an arbitrary widget such as the field a user just tapped.

What the editor can express

FlutterFlow's Bottom Sheet action documents these options on the Show side: the component to display, Height, Background and Barrier Color, passed parameters, Non Blocking, Non Dismissible, Enable Drag and an Action Output Variable Name. Height is described as a cap on how much of the screen the sheet takes: "You should set the height if you want the bottom sheet to appear only up to some portion of the screen."

Keyboard handling, scroll control and safe area are not documented on that page. Treat that as the boundary of what the action configures, not as proof that the generated sheet ignores insets, and check your own project's generated code if you need to know which parameters it passes.

Three editor settings do real work here, and one of them is frequently mistaken for a keyboard fix:

  • Column scroll behavior is the setting that matters most, because it is what gives the focused field a scrollable to move inside.
  • Hide Keyboard on Tap, under Advanced Configurations in the page properties, "makes the keyboard retract when the user taps anywhere outside the keyboard area on the screen", as the page properties documentation puts it. Useful, and unrelated to whether the submit button is reachable while the keyboard is up. The Safe Area setting in the same panel is about system UI too: the docs describe it as keeping page content from overlapping the status bar, which is the opposite edge.
  • Is On-Screen Keyboard Visible, a global property that "checks if the on-screen or soft keyboard is visible". The documented example wires it to conditional visibility so a custom bottom navigation bar hides while the keyboard is displayed. The same pattern suits a decorative sheet header you would rather drop on a short screen. It gives you a boolean, not a measurement.

Where this runs out is the sheet height. A single Height value cannot shrink when the keyboard claims a third of a short window, and the editor documents no inset-aware expression for it. That is the point where custom code earns its place, and my notes on when a FlutterFlow project should drop to pure Flutter cover the plan and billing side of that decision.

A bounded sheet that survives the inset

The following is a reference implementation assembled from the API documentation cited above. It was not executed in this session, so treat it as a starting point to compile and check against your own layout rather than as a tested snippet.

// Reference implementation. Not executed in this article.
Future<void> showBoundedFormSheet(BuildContext context) {
  return showModalBottomSheet<void>(
    context: context,
    isScrollControlled: true, // lifts the 9 / 16 max-height ratio that
                              // applies while scroll control is off
    builder: (sheetContext) {
      final media = MediaQuery.of(sheetContext);
      final inset = media.viewInsets.bottom;
      // What the window has left once the keyboard has taken its band.
      final available = media.size.height - inset;
      // Leave a strip of the window visible so the sheet reads as a sheet.
      final maxHeight = available > 0 ? available * 0.9 : 0.0;

      return ConstrainedBox(
        constraints: BoxConstraints(maxHeight: maxHeight),
        child: SingleChildScrollView(
          // The keyboard band, plus room for the submit control to clear it.
          padding: EdgeInsets.only(bottom: inset + 24),
          child: const FormBody(),
        ),
      );
    },
  );
}

Three choices in there carry the behavior.

isScrollControlled defaults to false, and the documentation advises you to "consider setting this parameter to true if this bottom sheet has a scrollable child". While it is false, the sheet is capped: scrollControlDisabledMaxHeightRatio is documented as "the max height constraint ratio for the bottom sheet when isScrollControlled is set to false", defaulting to 9 / 16, and the same page states that "no ratio will be applied when isScrollControlled is set to true". So a form sheet that needs most of a short screen has to opt in, and a sheet stuck at a little over half the window with space above it is usually this default rather than your layout.

The maxHeight is 90% of what the window has left after the inset, rather than a pixel constant, which is what keeps it sensible in landscape. The guard against a negative value matters there: a short landscape window with a tall keyboard can leave nothing, and BoxConstraints will not accept a negative maximum. And the bottom padding goes on the scroll view rather than on the sheet. Since that padding insets the child the scroll view scrolls, the keyboard band becomes extra scrollable extent at the end of the form instead of taking usable height away from the sheet.

One documented detail matters before you nest a SafeArea in the sheet. useSafeArea defaults to false, it avoids intrusions on the top, left and right rather than the bottom, and while it is false MediaQuery.removePadding is applied to the top, so a SafeArea inside the sheet will not avoid top intrusions either. Set it if your sheet can grow tall enough to reach a notch. It is not a keyboard setting.

If you build this as a custom widget instead of editing an export, the parameter and state rules are the awkward part, and how a FlutterFlow custom widget receives parameters and sends values back covers them separately.

What happens when the keyboard closes

The inset animates back to zero, every widget that read it through MediaQuery rebuilds, and three things can go wrong in that direction rather than the other one.

The sheet can stay tall and leave a band of empty space where the keyboard was, if you bounded the height but kept the content pinned to the bottom. The scroll position can sit past the new maximum extent and snap, which reads as a flicker. And a footer on the default SafeArea regains its gap at that moment, because padding returns to the full viewPadding once the inset is gone, so a layout that fit exactly while the keyboard was up is suddenly taller.

Check the close as deliberately as the open: dismiss the keyboard with the sheet still open, which is what Hide Keyboard on Tap does on an outside tap, and watch the last control rather than the first field.

Six small phone layouts in one grid: different widths, one in landscape, three carrying the keyboard band and three without it, one with larger type and fewer fields, and one with a message wrapped onto three lines.

The states to check before calling it fixed

The builder canvas has a Display Keyboard option, under Builder Settings in the Device Preview Settings, that the canvas documentation introduces as showing the keyboard on the preview screen, and a device picker for previewing other dimensions. Use both while you lay the sheet out. They are a builder preview, and the same page describes Show Overflows in terms of how errors "will appear in Test Mode", so the final check belongs on something that raises a real keyboard.

StateWhat to watchPass condition
320 px width, keyboard openSubmit controlReachable by scrolling, not clipped
360, 390, 414 and 430 px, keyboard openField spacing and the last controlNo horizontal page scroll, nothing hidden
Landscape, keyboard openSheet heightSheet still shows at least two fields and scrolls
Largest system text scaleLabels, button text, hint textLegible, no clipped glyph, no overflow
Long validation message, three linesThe field that failedMessage fully visible with the field
Keyboard dismissed, sheet openFooter and scroll extentNo empty band, no snap, footer gap returns

Large text is a release requirement rather than a nicety. The Flutter accessibility release checklist states it as "Scale factors. The UI should remain legible and usable at very large scale factors for text size and display scaling." A sheet whose height is tuned to one font size is the layout most likely to fail that check, which is another reason to bound the height as a fraction of the window and let the content scroll.

Two shortcuts make this table pass while the product gets worse. Locking the text scale makes large text fit and breaks the checklist item above. A fixed pixel height makes one device correct and the rest wrong, in a way that looks like a device bug rather than a layout decision.

Limits of this approach

A bounded, scrollable sheet with inset-aware padding covers the four responsibilities in the first table. It does not rescue a form that does not belong in a sheet. Past a certain number of fields, a full page with its own Scaffold gets the body resize behavior for free and a title row that does not compete with a drag handle.

The 90% fraction in the snippet is a choice, not a documented value, and the 24 logical pixels of clearance under the inset are a choice too. Pick both from your own sheet's last control and header, and keep them expressed against the window rather than against one device.

One more constraint comes with the scroll view FlutterFlow generates. The SingleChildScrollView documentation warns that if the viewport "is expected to usually contain content beyond the dimensions of the screen, then SingleChildScrollView would be very expensive", and points to ListView instead. A six-field form is fine. A sheet holding a long list of selectable rows is the case where the widget choice itself matters, not just the padding on it.

Nothing here was measured on a device while writing. The behavior of viewInsets, padding, viewPadding, scrollPadding, the Scaffold resize and the sheet ratio comes from the documentation linked at each claim, and the editor options come from FlutterFlow's current docs pages. The snippet is written against those APIs; the states table is how you find out what your own layout does with them.

Open your sheet on the shortest screen you support, with the keyboard up and the system text scale at its largest, and read off which of the four responsibilities fails first. Which one was it: nothing scrolls at all, or everything scrolls and the button still will not clear the keyboard?

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

Why does the keyboard cover the button in a FlutterFlow bottom sheet?

Because a modal bottom sheet is pushed as its own route, so the Scaffold property that resizes a page body around the keyboard does not apply to it. Scaffold.resizeToAvoidBottomInset defaults to true and its documented scope is the body and the scaffold's floating widgets, which a sheet route is not. Anything the sheet does about the keyboard has to come from its own content: a scrollable body so the last control can be reached, a height bounded against the window rather than a pixel constant, and bottom padding equal to the current bottom inset so the control can clear the keyboard band.

How do you read the keyboard inset inside a FlutterFlow bottom sheet?

Read MediaQuery.of(context).viewInsets.bottom inside the sheet builder, which gives the logical pixels the keyboard currently occupies. Flutter's documentation says to go through MediaQuery rather than the view directly, because that makes every widget depending on the inset rebuild when the value changes, which is what lets the sheet follow the keyboard animation instead of jumping once at the end. The FlutterFlow editor documents no inset value of its own on the Bottom Sheet action. It does document a global property called Is On-Screen Keyboard Visible, but that returns a boolean for conditional visibility rather than a measurement you can use in a height.

How do you scroll a focused input above the keyboard in FlutterFlow?

Make the sheet content scrollable and the field will move itself. TextField.scrollPadding is documented as inset for the edges surrounding a Scrollable when the field scrolls into view, defaulting to EdgeInsets.all(20.0), and a focused field that is not completely visible attempts to make itself visible by scrolling a surrounding Scrollable. The condition is that surrounding scrollable. In FlutterFlow that means setting the Column to Allow Scrolling, which generates a SingleChildScrollView around it. The Scroll To and Scroll To Index actions are not a substitute: they are documented for moving a ListView, StaggeredView or GridView, not for scrolling to the field a user just tapped.

Does a fixed sheet height fix a form hidden by the keyboard?

No, it moves the failure to another device. A fixed pixel height cannot shrink when the keyboard claims part of a short window, so the sheet that fits one phone overflows or strands its submit control on the next one. Derive the maximum height from the window instead, subtract the current bottom inset, and guard against a negative value, because a short landscape window with a tall keyboard can leave nothing and BoxConstraints will not accept a negative maximum. One documented default is worth knowing here: while isScrollControlled is false a modal sheet is capped at a ratio of 9 / 16, and no ratio applies once it is true.

Which states should you check after fixing a keyboard layout?

Check widths from 320 to 430 pixels with the keyboard open, landscape with the keyboard open, the largest system text scale, a validation message long enough to wrap onto three lines, and the moment the keyboard is dismissed while the sheet stays open. The builder canvas helps while you lay the sheet out, since it has a Display Keyboard option and a device picker, but it is a preview and the final check belongs on a target that raises a real keyboard. Large text is not optional polish: Flutter's accessibility release checklist asks that the UI stay legible and usable at very large scale factors.