When highlighting texts in Textfield => It shows two highlights and when scrolling, the other highlights remain at the same position
When highlighting texts: When highlighting texts
When scrolling: When scrolling
This only happens on Flutter web, so I guess the reason is that the different behavior between flutter Canvas and web DOM.
Environment
Flutter 3.38.2 • channel stable • https://github.com/flutter/flutter.git
Framework • revision f5a8537f90 (5 days ago) • 2025年11月18日 09:27:21 -0500
Engine • hash 78c3c9557e50ee7c676fa37562558c59efd8406a (revision b5990e5ccc) (10 days ago) •
2025年11月12日 21:08:24.000Z
Tools • Dart 3.10.0 • DevTools 2.51.1
Safari on iOS simulator ( iPhone 13 Pro, iOS 18.6 )
Chrome on iPhone 13 Pro Max, iOS 18.7.2
Related Codes
- main.dart
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb, kReleaseMode;
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'firebase_options.dart' as prod;
import 'firebase_options_stg.dart' as stg;
import 'firebase_emulator.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/app_localizations.dart';
import 'client/screens/start_page.dart';
import 'client/screens/login_page.dart';
import 'client/screens/signup_page.dart';
import 'client/screens/questions_page.dart';
import 'client/screens/home_page.dart';
import 'auth_required.dart';
import 'client/screens/plan_proposal_page.dart';
import 'client/screens/settings_page.dart';
import 'client/screens/weight_history_screen.dart';
import 'coach/screens/coach_dashboard_page.dart';
import 'coach/screens/coach_login_page.dart';
import 'utils/auth_redirect_wrapper.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Select Firebase options by build-time environment.
// - Debug/Profile: default to "stg" unless FIREBASE_ENV is explicitly provided
// - Release: default to "prod" unless FIREBASE_ENV is explicitly provided
const String envDefine = String.fromEnvironment(
'FIREBASE_ENV',
defaultValue: '',
);
final String env = envDefine.isEmpty
? (kReleaseMode ? 'prod' : 'stg')
: envDefine;
final FirebaseOptions firebaseOptions = env == 'stg'
? stg.DefaultFirebaseOptions.currentPlatform
: prod.DefaultFirebaseOptions.currentPlatform;
await Firebase.initializeApp(options: firebaseOptions);
await connectFirebaseToEmulatorIfNeeded();
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
const bool useEmulator = bool.fromEnvironment(
'USE_FIREBASE_EMULATOR',
// Default to emulator in non-release builds for safety
defaultValue: !kReleaseMode,
);
final double bottom = (kIsWeb && useEmulator) ? 40.0 : 0.0;
if (bottom == 0.0) return child!;
return Padding(
padding: EdgeInsets.only(bottom: bottom),
child: child,
);
},
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
theme: () {
const double webLineHeight = 1.3;
final baseTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
fontFamily: 'NotoSansJP',
);
final TextTheme baseTextTheme = baseTheme.textTheme;
final TextTheme adjustedTextTheme = kIsWeb
? baseTextTheme.copyWith(
// Fix line height for all text styles on web to align browser and Flutter selection highlights
displayLarge: baseTextTheme.displayLarge?.copyWith(
height: webLineHeight,
),
displayMedium: baseTextTheme.displayMedium?.copyWith(
height: webLineHeight,
),
displaySmall: baseTextTheme.displaySmall?.copyWith(
height: webLineHeight,
),
headlineLarge: baseTextTheme.headlineLarge?.copyWith(
height: webLineHeight,
),
headlineMedium: baseTextTheme.headlineMedium?.copyWith(
height: webLineHeight,
),
headlineSmall: baseTextTheme.headlineSmall?.copyWith(
height: webLineHeight,
),
titleLarge: baseTextTheme.titleLarge?.copyWith(
height: webLineHeight,
),
titleMedium: baseTextTheme.titleMedium?.copyWith(
height: webLineHeight,
),
titleSmall: baseTextTheme.titleSmall?.copyWith(
height: webLineHeight,
),
bodyLarge: baseTextTheme.bodyLarge?.copyWith(
height: webLineHeight,
),
bodyMedium: baseTextTheme.bodyMedium?.copyWith(
height: webLineHeight,
),
bodySmall: baseTextTheme.bodySmall?.copyWith(
height: webLineHeight,
),
labelLarge: baseTextTheme.labelLarge?.copyWith(
height: webLineHeight,
),
labelMedium: baseTextTheme.labelMedium?.copyWith(
height: webLineHeight,
),
labelSmall: baseTextTheme.labelSmall?.copyWith(
height: webLineHeight,
),
)
: baseTextTheme;
return baseTheme.copyWith(
textTheme: adjustedTextTheme,
primaryTextTheme: adjustedTextTheme,
);
}(),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
AppLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
initialRoute: '/start',
routes: {
'/start': (_) => const AuthRedirectWrapper(child: StartPage()),
'/login': (_) => const AuthRedirectWrapper(child: LoginPage()),
'/signup': (_) => const SignUpPage(),
'/questions': (_) => const QuestionsPage(),
'/home': (_) =>
const AuthRequired(requiredRole: 'client', child: HomePage()),
'/planProposal': (_) => const PlanProposalPage(),
'/settings': (_) =>
const AuthRequired(requiredRole: 'client', child: SettingsPage()),
'/weightHistory': (_) => const AuthRequired(
requiredRole: 'client',
child: WeightHistoryScreen(),
),
'/coach/login': (_) =>
const AuthRedirectWrapper(child: CoachLoginPage()),
'/coach/dashboard': (_) => const AuthRequired(
requiredRole: 'coach',
child: CoachDashboardPage(),
),
},
);
}
}
- web/index.html
<!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="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="nutrition_coaching_app">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>nutrition_coaching_app</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
- Textfield section where the issue is happening
TextField(
controller: _controller,
maxLines: null,
decoration: const InputDecoration(border: OutlineInputBorder()),
style: Theme.of(context).textTheme.bodyMedium,
),
Note: I've tried the following 2 ways to add to <head> section in index.html, but neither of them worked
-
<style>
/* Keep Flutter highlight; hide browser selection for hidden editing element */
.flt-text-editing::selection {
background-color: transparent !important;
color: transparent !important; /* Hide text, keep caret logic working */
}
.flt-text-editing::-moz-selection {
background-color: transparent !important;
color: transparent !important;
}
</style> -
\<style\> /\* Align hidden editing element font metrics with Flutter TextField \*/ .flt-text-editing { font-family: "NotoSansJP", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; font-size: 14px !important; /\* Match bodyMedium default fontSize \*/ line-height: 1.3 !important; /\* Match TextStyle.height: 1.3 \*/ letter-spacing: 0 !important; /\* Default letter spacing \*/ } \</style\>