-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Description
WhatsApp.Video.2025-12-09.at.12.30.00.mp4
When using Flutter Web on mobile browsers (especially Chrome on Android), opening the on-screen keyboard for a TextField/TextFormField causes the whole Flutter view to shift up and leaves a persistent grey/white gap between the focused text field and the top of the keyboard.
The issue is reproducible with a simple form page:
Build and deploy a Flutter Web app.
Open it in a mobile browser on Android (and optionally iOS).
Scroll to a TextFormField near the bottom and tap it to focus.
The keyboard appears, but the form content is pushed too high and a blank grey area remains above the keyboard.
Expected behavior:
The focused text field should sit just above the keyboard with no extra empty space, and the viewport should resize or scroll smoothly so the form remains aligned.
Actual behavior:
The viewport shifts incorrectly, leaving 30–100 px (varies by device) of unused grey/white space between the last visible widget and the keyboard. In some cases, this offset persists or worsens after closing and reopening the keyboard, making the form look broken and harder to use.
import 'package:flutter/material.dart';
class ContactFormPage extends StatefulWidget {
const ContactFormPage({Key? key}) : super(key: key);
@override
State<ContactFormPage> createState() => _ContactFormPageState();
}
class _ContactFormPageState extends State<ContactFormPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
final _companyController = TextEditingController();
final _subjectController = TextEditingController();
final _addressController = TextEditingController();
final _cityController = TextEditingController();
final _websiteController = TextEditingController();
final _messageController = TextEditingController();
bool _isSubmitted = false;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_phoneController.dispose();
_companyController.dispose();
_subjectController.dispose();
_addressController.dispose();
_cityController.dispose();
_websiteController.dispose();
_messageController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
setState(() {
_isSubmitted = true;
});
// Simulate form submission
Future.delayed(const Duration(seconds: 2), () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Form submitted successfully!'),
backgroundColor: Colors.green,
),
);
// Reset form
_formKey.currentState!.reset();
_nameController.clear();
_emailController.clear();
_phoneController.clear();
_companyController.clear();
_subjectController.clear();
_addressController.clear();
_cityController.clear();
_websiteController.clear();
_messageController.clear();
setState(() {
_isSubmitted = false;
});
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: SingleChildScrollView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Get in Touch',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Fill out the form below and we\'ll get back to you',
style: TextStyle(fontSize: 14, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
hintText: 'Enter your name',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(
labelText: 'Address',
hintText: 'Enter your address',
prefixIcon: Icon(Icons.location_on),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your address';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _cityController,
decoration: const InputDecoration(
labelText: 'City',
hintText: 'Enter your city',
prefixIcon: Icon(Icons.location_city),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your city';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _websiteController,
decoration: const InputDecoration(
labelText: 'Website',
hintText: 'Enter your website URL',
prefixIcon: Icon(Icons.language),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your website';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'Phone Number',
hintText: 'Enter your phone number',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your phone number';
}
if (value.length < 10) {
return 'Please enter a valid phone number';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _companyController,
decoration: const InputDecoration(
labelText: 'Company Name',
hintText: 'Enter your company name',
prefixIcon: Icon(Icons.business),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your company name';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _subjectController,
decoration: const InputDecoration(
labelText: 'Subject',
hintText: 'Enter subject',
prefixIcon: Icon(Icons.subject),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a subject';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email Address',
hintText: 'Enter your email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!RegExp(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
).hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _messageController,
decoration: const InputDecoration(
labelText: 'Message',
hintText: 'Enter your message',
prefixIcon: Icon(Icons.message),
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a message';
}
if (value.length < 10) {
return 'Message must be at least 10 characters';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isSubmitted ? null : _submitForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isSubmitted
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Submit', style: TextStyle(fontSize: 16)),
),
],
),
),
),
),
),
);
}
}
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="textfield_issue_web">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>textfield_issue_web</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async>
window.flutterConfiguration = {
renderer: "html"
};
</script>
</body>
</html>