Repository navigation
Cupertino in dark mode is too low-contrast and theme APIs are missing #80860
Copy link
Copy link
Closed
Labels
a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)Accessibility, e.g. VoiceOver or TalkBack. (aka a11y)frameworkflutter/packages/flutter repository. See also f: labels.flutter/packages/flutter repository. See also f: labels.p: cupertino_uicupertino_ui package in flutter/packagescupertino_ui package in flutter/packagesteam-designOwned by Design Languages teamOwned by Design Languages teamtriaged-designTriaged by Design Languages teamTriaged by Design Languages team
Description
Use case
I'm writing an app with iOS style using Flutter's Cupertino library. I want the app to automatically switch between light and dark modes.
Proposal
- Fix the Cupertino widgets to be usable in dark mode.
- Add support to Flutter for dark mode themes.
- Provide a working example somewhere in the documentation.
Existing APIs with Missing or Undocumented Dark Mode Support
- The
MaterialAppconstructor takes boththemeanddarkThemearguments. TheCupertinoAppconstructor takes onlytheme. CupertinoThemetakes a singleCupertinoThemeDatawhich can represent either a light or a dark color scheme, but not both.ThemeandThemeDatahave the same limitation.CupertinoDynamicColorseems able to choose specific colors for light and dark modes. The docs are very difficult to understand. For example, it talks about "manually resolving a color" but I have no idea what this means.- One cannot use
MediaQuery.of(ctx).platformBrightnessto pick the theme passed toCupertinoAppbecause there is no parentMediaQuerywidget. Sadly, one cannot just add a newMediaQuerywidget above theCupertinoAppwidget because the code to query the system's settings is hidden inwidgets/app.dart:_MediaQueryFromWindow. - Workaround code for issue [cupertino] Text is rendered black (so not visible) by default in dark mode #48438
Mostly-Working Example
Problems in Dark Mode:
- Disabled button text is unreadable.
- Picker background is faint. It looks like a label, not a picker.
- Slider track is very faint.
- Edit box placeholder text is too faint to see.
- Alert dialog border is invisible (Make CupertinoAlertDialog clearly visible in dark mode #80921)
- Keyboard is too bright on Android.
code sample
// main.dart
import 'package:flutter/cupertino.dart'
show
showCupertinoDialog,
CupertinoAlertDialog,
CupertinoApp,
CupertinoButton,
CupertinoDatePicker,
CupertinoDialogAction,
CupertinoIcons,
CupertinoNavigationBar,
CupertinoPage,
CupertinoPageScaffold,
CupertinoPicker,
CupertinoSlider,
CupertinoSwitch,
CupertinoTextField,
CupertinoTheme,
CupertinoThemeData;
import 'package:flutter/foundation.dart'
show
debugBrightnessOverride,
kReleaseMode;
import 'package:flutter/widgets.dart'
show
runApp,
Builder,
BuildContext,
Center,
Color,
Column,
Container,
DefaultTextStyle,
EdgeInsets,
Icon,
Key,
MainAxisSize,
MediaQuery,
MediaQueryData,
Page,
Row,
Spacer,
State,
StatefulWidget,
StatelessWidget,
Text,
TextAlign,
TextEditingController,
Navigator,
Widget,
WidgetsBinding,
WidgetsBindingObserver;
import 'dart:ui' show Brightness;
// Copied from flutter/lib/src/widgets/app.dart:_MediaQueryFromWindow .
class MediaQueryFromWindow extends StatefulWidget {
const MediaQueryFromWindow({Key key, this.child}) : super(key: key);
final Widget child;
@override
MediaQueryFromWindowsState createState() => MediaQueryFromWindowsState();
}
class MediaQueryFromWindowsState extends State<MediaQueryFromWindow>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAccessibilityFeatures() {
setState(() {});
}
@override
void didChangeMetrics() {
setState(() {});
}
@override
void didChangeTextScaleFactor() {
setState(() {});
}
@override
void didChangePlatformBrightness() {
setState(() {});
}
@override
Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(
WidgetsBinding.instance.window);
if (!kReleaseMode) {
data = data.copyWith(platformBrightness: debugBrightnessOverride);
}
return MediaQuery(
data: data,
child: widget.child,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
Page rootPage() =>
CupertinoPage(
title: 'Root Title',
child: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Text('Root'),
),
);
class CupertinoWidgetsPageWidget extends StatelessWidget {
TextEditingController controller = new TextEditingController(
text: "contents1");
@override
Widget build(BuildContext ctx) =>
CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoButton(
padding: EdgeInsets.symmetric(
horizontal: 4.0),
child: Text(
'Disabled1',
textAlign: TextAlign.center,
),
),
CupertinoButton(
padding: EdgeInsets.symmetric(
horizontal: 4.0),
onPressed: () => {},
child: Text(
'Action1',
textAlign: TextAlign.center,
),
),
],
),
),
child: Column(
children: [
Text('Text1'),
Icon(
CupertinoIcons.right_chevron,
size: 24.0,
semanticLabel: 'Chevron',
),
CupertinoButton(
child: Text('Enabled Button'),
onPressed: () =>
showCupertinoDialog(
context: ctx,
builder: (BuildContext ctx) =>
CupertinoAlertDialog(
title: Text('Alert Title'),
content: Text('Content1'),
actions: [
CupertinoDialogAction(
isDefaultAction: false,
isDestructiveAction: false,
child: Text('Disabled1'),
),
CupertinoDialogAction(
isDefaultAction: false,
isDestructiveAction: false,
onPressed: () =>
Navigator.pop(
ctx, false),
child: Text('Action1'),
),
CupertinoDialogAction(
isDefaultAction: false,
isDestructiveAction: true,
onPressed: () =>
Navigator.pop(
ctx, false),
child: Text('Destructive1'),
),
CupertinoDialogAction(
isDefaultAction: true,
isDestructiveAction: false,
onPressed: () =>
Navigator.pop(
ctx, false),
child: Text('Cancel'),
),
],
),
),
),
CupertinoButton(
child: Text('Disabled Button')),
Row(children: [
CupertinoSwitch(value: true),
CupertinoSwitch(value: false),
CupertinoSwitch(
value: true,
onChanged: (x) => {},
),
CupertinoSwitch(
value: false,
onChanged: (x) => {},
),
]),
CupertinoPicker(
itemExtent: 30.0,
onSelectedItemChanged: (x) => {},
children: [
Text('Item1'),
Text('Item2'),
Text('Item3'),
Text('Item4'),
],
),
CupertinoSlider(
value: 0.0,
divisions: 3,
onChanged: (x) => {},
),
Container(
height: 100.0,
child:
CupertinoDatePicker(
onDateTimeChanged: (x) => {},
),
),
CupertinoTextField(
placeholder: "placeholder1"),
CupertinoTextField(
controller: this.controller),
Spacer(),
],
),
);
}
Page cupertinoWidgetsPage() =>
CupertinoPage(
title: 'Title1',
child: CupertinoWidgetsPageWidget());
class AppWidget extends StatelessWidget {
@override
Widget build(BuildContext ctx) {
final isLight = MediaQuery
.of(ctx)
.platformBrightness == Brightness.light;
final cupertinoThemeData = isLight ? CupertinoThemeData(
// Light
primaryColor: Color(0xff664600),
barBackgroundColor: Color(0xffFFF0D0),
scaffoldBackgroundColor: Color(0xffFFFFFF),
) : CupertinoThemeData(
// Dark
primaryColor: Color(0xffFFF0D0),
barBackgroundColor: Color(0xff664600),
// Has no effect on any Cupertino* widgets.
primaryContrastingColor: Color(0xffFF0000),
);
return CupertinoApp(
theme: cupertinoThemeData,
title: 'App1',
home: Builder(builder: (BuildContext ctx) =>
DefaultTextStyle(
style: CupertinoTheme
.of(ctx)
.textTheme
.textStyle,
child: Navigator(
pages: [rootPage(), cupertinoWidgetsPage()],
onPopPage: (route, result) => true,
),
),
),
);
}
}
void main() {
runApp(MediaQueryFromWindow(child: AppWidget()));
}| iOS dark | iOS light |
|---|---|
![]() |
![]() |
![]() |
![]() |
| Android dark | Android light |
|---|---|
![]() |
![]() |
![]() |
![]() |
EDIT 1: Mention MediaQuery.of(ctx).platformBrightness.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)Accessibility, e.g. VoiceOver or TalkBack. (aka a11y)frameworkflutter/packages/flutter repository. See also f: labels.flutter/packages/flutter repository. See also f: labels.p: cupertino_uicupertino_ui package in flutter/packagescupertino_ui package in flutter/packagesteam-designOwned by Design Languages teamOwned by Design Languages teamtriaged-designTriaged by Design Languages teamTriaged by Design Languages team








Activity
mleonhard commentedon Apr 22, 2021
EDIT 2: Updated to use a copy of
flutter/lib/src/widgets/app.dart:_MediaQueryFromWindowand then use aMediaQueryto pickCupertinoThemeData.HansMuller commentedon Apr 23, 2021
CC @LongCatIsLooong
mleonhard commentedon Sep 9, 2021
I still need this.
[-]Document how to set up Cupertino light/dark colors[/-][+]Fix Cupertino widgets in dark mode or document workarounds[/+][-]Fix Cupertino widgets in dark mode or document workarounds[/-][+]Make Cupertino widgets usable in dark mode or document workarounds[/+]mleonhard commentedon Dec 14, 2021
EDIT3: Change title and section headings to clearly state that this is a problem with the widget implementations, Flutter API, and missing documentation. Cupertino dark mode support is incomplete.
workerbee22 commentedon Apr 16, 2022
Very much support this, given iOS support for light and dark mode is no longer very new, Flutter should properly support iOS. Looking for feature parity between 'MaterialApp' and 'CupertinoApp' as well as the theme classes.
5 remaining items
[-]Make Cupertino widgets usable in dark mode or document workarounds[/-][+]Cupertino in dark mode is too low-contrast and theme APIs are missing[/+]gnprice commentedon May 16, 2023
Adjusted title and labels because in general we fix bugs rather than document workarounds for them:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#lazy-programming
(It's not clear there even are workarounds for these that don't amount to just fixing the bug.)
In particular, all of the items under "Problems in Dark Mode" appear to be that something is too dark or too light. It's hard to evaluate without screenshots of iOS native widgets to compare to; but for one item (the alert dialog), #80921 makes clear that indeed the problem is too dark a dialog background causing too little contrast with the overall background.
(The next step remains, as @Hixie said above, for someone who wants to make progress on one or more of these issues to file them as separate issue threads.)
Piinks commentedon May 21, 2025
I am going to close this based on earlier discussion about lacking specific actionable issues. We have fixed some of those referred to here, like #80921, and some other dark mode related fixes have referenced this issue. If there is a specific scenario we need to fix, please file an issue with a reproduction. Thank you!
github-actions commentedon Jun 4, 2025
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.