Skip to content

Cupertino in dark mode is too low-contrast and theme APIs are missing #80860

Closed

Description

@mleonhard

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

  1. Fix the Cupertino widgets to be usable in dark mode.
  2. Add support to Flutter for dark mode themes.
  3. Provide a working example somewhere in the documentation.

Existing APIs with Missing or Undocumented Dark Mode Support

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
Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 57 25 Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 58 17
Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 57 46 Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 58 23
Android dark Android light
Screenshot_1619071226 Screenshot_1619071175
Screenshot_1619071230 Screenshot_1619071188

EDIT 1: Mention MediaQuery.of(ctx).platformBrightness.

Activity

  1. added
    in triagePresently being triaged by the triage team
    p: cupertino_uicupertino_ui package in flutter/packages
    frameworkflutter/packages/flutter repository. See also f: labels.
    and removed
    in triagePresently being triaged by the triage team
    on Apr 21, 2021
  2. mleonhard commented on Apr 22, 2021

    @mleonhard
    ContributorAuthor

    EDIT 2: Updated to use a copy of flutter/lib/src/widgets/app.dart:_MediaQueryFromWindow and then use a MediaQuery to pick CupertinoThemeData.

  3. HansMuller commented on Apr 23, 2021

    @HansMuller
    Contributor
  4. mleonhard commented on Sep 9, 2021

    @mleonhard
    ContributorAuthor

    I still need this.

  5. changed the title [-]Document how to set up Cupertino light/dark colors[/-] [+]Fix Cupertino widgets in dark mode or document workarounds[/+] on Dec 14, 2021
  6. changed the title [-]Fix Cupertino widgets in dark mode or document workarounds[/-] [+]Make Cupertino widgets usable in dark mode or document workarounds[/+] on Dec 14, 2021
  7. mleonhard commented on Dec 14, 2021

    @mleonhard
    ContributorAuthor

    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.

  8. workerbee22 commented on Apr 16, 2022

    @workerbee22

    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.

  9. 5 remaining items

  10. changed the title [-]Make Cupertino widgets usable in dark mode or document workarounds[/-] [+]Cupertino in dark mode is too low-contrast and theme APIs are missing[/+] on May 16, 2023
  11. added
    a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)
    and removed on May 16, 2023
  12. gnprice commented on May 16, 2023

    @gnprice
    Member

    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.)

  13. Piinks commented on May 21, 2025

    @Piinks
    Contributor

    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!

  14. github-actions commented on Jun 4, 2025

    @github-actions

    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 -v and a minimal reproduction of the issue.

  15. locked as resolved and limited conversation to collaborators on Jun 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)frameworkflutter/packages/flutter repository. See also f: labels.p: cupertino_uicupertino_ui package in flutter/packagesteam-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions