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.
| Responsibility | What it controls | Failure you see |
|---|---|---|
| Keyboard inset | How much of the bottom of the window the keyboard occupies | The last control sits under the keyboard and nothing moves it |
| Scrollable content | Whether content taller than the sheet can be reached | Content is cut off and the sheet refuses to scroll |
| Focus movement | Whether the field you just tapped ends up visible | You type into a field you cannot see |
| Sheet height | How much of the window the sheet is allowed to take | The 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:
- 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.
- 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. - 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.
- 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".
- 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.

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:
SafeAreainsets bypadding, 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 bottomviewPaddinginstead of the bottompadding.
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.

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.
| State | What to watch | Pass condition |
|---|---|---|
| 320 px width, keyboard open | Submit control | Reachable by scrolling, not clipped |
| 360, 390, 414 and 430 px, keyboard open | Field spacing and the last control | No horizontal page scroll, nothing hidden |
| Landscape, keyboard open | Sheet height | Sheet still shows at least two fields and scrolls |
| Largest system text scale | Labels, button text, hint text | Legible, no clipped glyph, no overflow |
| Long validation message, three lines | The field that failed | Message fully visible with the field |
| Keyboard dismissed, sheet open | Footer and scroll extent | No 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?
