Skip to content

Add dark mode to the Flutter blog - #13954

Open
conooi wants to merge 4 commits into
flutter:mainfrom
conooi:blog-dark-mode
Open

conooi wants to merge 4 commits into
flutter:mainfrom
conooi:blog-dark-mode

Conversation

@conooi

@conooi conooi commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Adds dark mode support (Light, Dark, and Automatic) to the Flutter blog (flutter.dev/blog), matching the theme switcher experience on dart.dev and docs.flutter.dev:

  • Header theme switcher: Renders NestedThemeSwitcher in the flutter.dev header when viewing /blog pages and auto-closes the dropdown menu on selection.
  • Pre-paint theme initialization & persistence: Extracts shared themeInitScript and themeSyncBodyScript helpers in package:site_shared so both DashLayout and BlogLayout apply the stored localStorage['theme'] preference before first paint and stay in sync across browser back/forward navigation (pageshow), cross-tab storage events, and live OS prefers-color-scheme changes.
  • Scoped blog dark theme styles: Adds dark mode CSS custom properties, .opal dark syntax highlighting overrides, header/footer dark styling, and diagram/icon contrast safeguards (--site-diagram-wrap-bgColor, .light-mode-visible, .dark-mode-visible, .theme-icon) scoped to body.blog in _blog_page.scss without affecting other flutter.dev marketing pages.
  • Hydration optimization: Skips redundant .blog-card DOM class mutations in BlogCategories during initial hydration when the default all view is already server-rendered.

Fixes #13942

@conooi
conooi requested a review from parlough October 2, 2026 22:56
@conooi
conooi requested review from a team, abdallahshaban557 and sfshaza2 as code owners October 2, 2026 22:56

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a comprehensive dark mode theme system for the blog, introducing a NestedThemeSwitcher component, centralizing theme initialization and synchronization scripts, and updating layouts, components, and SCSS styles to support dark mode. Feedback on the changes highlights a style guide violation where unawaited calls in ThemeSwitcher.dispose should include explanatory comments.

Comment on lines +85 to +87
unawaited(_pageShowSubscription?.cancel());
unawaited(_storageSubscription?.cancel());
unawaited(_mediaQuerySubscription?.cancel());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide, any futures that are deliberately detached with unawaited must include a comment explaining why. Please add a brief comment explaining why these subscriptions are cancelled asynchronously.

Suggested change
unawaited(_pageShowSubscription?.cancel());
unawaited(_storageSubscription?.cancel());
unawaited(_mediaQuerySubscription?.cancel());
// Cancel subscriptions asynchronously on dispose.
unawaited(_pageShowSubscription?.cancel());
unawaited(_storageSubscription?.cancel());
unawaited(_mediaQuerySubscription?.cancel());
References
  1. Futures should be awaited, returned, or deliberately detached with unawaited and a comment explaining why. (link)

@flutter-website-bot

flutter-website-bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Staged preview of the updated docs.flutter.dev site (updated for commit 5709fd4):

https://flutter-docs-prod--docs-pr13954-blog-dark-mode-97n6shd9.web.app

@flutter-website-bot

flutter-website-bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Staged preview of the updated flutter.dev site (updated for commit 5709fd4):

https://flutter-dev-230821--www-pr13954-blog-dark-mode-tcpu8yil.web.app

@parlough parlough self-assigned this Oct 5, 2026

@parlough parlough left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for exploring this @conooi!

Generally looks great and like a good direction, with most of my remaining concerns about generalizing the style updates so future updates and maintenance are easier. Perhaps the capability can be expanded in the future as well.

Let me know if you have any questions or if you'd prefer I tackle any of the suggestions. Thanks again :D

final oppositeId = isDark ? _Theme.light.id : _Theme.dark.id;

for (final element in [
web.document.documentElement,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and elsewhere that documentElement is used (which I believe will resolve to the html element, can we instead just use web.document.body? That way we match the docs sites and there's a singular consistent location where the theme is configured. Then the styles can be simplified as well.

letter-spacing: normal;
}

#theme-switcher {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting this out to another dedicated file in the case we bring this to other parts of the site. These styles don't require the blog page.

Comment on lines +45 to +49
window.addEventListener('pageshow', applyStoredTheme);
window.addEventListener('storage', applyStoredTheme);
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', applyStoredTheme);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to listen to these events here since the Dart code already does. This script should be kept as focused initialization code like it was before. Similar to:

try {
  const storedTheme = window.localStorage.getItem('theme') ?? 'light-mode';
  const isAuto = storedTheme === 'auto-mode';
  const isDark = isAuto
      ? window.matchMedia('(prefers-color-scheme: dark)').matches
      : storedTheme === 'dark-mode';
  document.body.classList.remove('light-mode', 'dark-mode', 'auto-mode');
  document.body.classList.add(isDark ? 'dark-mode' : 'light-mode');
  if (isAuto) document.body.classList.add('auto-mode');
} catch (_) {
  // localStorage is not available; fall back to default light theme.
}

Then we don't need the later themeSyncBodyScript either.

Comment on lines +37 to +47
.text(
'html.dark-mode, '
'html.dark-mode body.blog, '
'html.dark-mode body.blog main { '
'background-color: #121317; color: #dcdcdc; color-scheme: dark; '
'} '
'html.dark-mode body.blog '
'header.site-header:not(.mobile-nav-open), '
'html.dark-mode body.blog .site-footer { '
'background-color: #1c1e27; '
'}',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this is for. If these styles are needed, I believe they can be included with the rest of them in the SCSS files. If there's a reason they need to be here, add a comment explaining why.

Comment on lines +23 to +25
List<Component> get leadingHeadElements => const [];

List<Component> get leadingBodyElements => const [];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we end up keeping these getters, please add API doc comments explaining what they are/how they are used.

}

:is(body.blog.dark-mode, html.dark-mode body.blog) {
header.site-header:not(.mobile-nav-open) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, rather than recreating custom header styles just for the blog specifically in dark mode, update the pre-existing/shared header styles to use and respect the theme-specific variables. The variables are only updated for the blog so it shouldn't result in any changes elsewhere and will be much easier to maintain. The same likely goes for other header, footer, and theme switcher styles here.

);
_storageSubscription = web.EventStreamProviders.storageEvent
.forTarget(web.window)
.listen((_) => _syncThemeFromStorage(updateState: true));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be made to specifically listen to changes to the theme key:

Suggested change
.listen((_) => _syncThemeFromStorage(updateState: true));
.listen((event) {
if (event.key == null || event.key == 'theme') {
_syncThemeFromStorage(updateState: true);
}
});

@conooi

conooi commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @parlough! I've updated the PR to address all of your feedback:

  • Consistent document.body theme classes & focused init script: Updated ThemeSwitcher and themeInitScript to only target document.body (matching docs.flutter.dev), removed themeSyncBodyScript and the duplicate JS event listeners, and filtered the storageEvent listener in ThemeSwitcher to the 'theme' key.
  • Layout cleanup: Moved themeInitScript into BlogLayout.leadingBodyElements, removed leadingHeadElements and the inline <style> tag, and added API doc comments to the DefaultLayout getters.
  • Generalized theme styles:
    • Extracted the #theme-switcher and .light-mode-visible / .dark-mode-visible styles into a dedicated lib/styles/components/_theme-switcher.scss file.
    • Updated the shared _header.scss and _footer.scss files to use the --site-* CSS variables and moved the body.dark-mode variable overrides to _vars.scss, removing all custom dark-mode header/footer overrides from _blog_page.scss.
  • Dark mode Flutter logo: Added flutter-logo-dark.svg (keeping the blue Flutter logomark with a white wordmark) for .dark-mode-visible in the header and footer so the icon mark doesn't turn solid white on dark backgrounds.

Ready for another look when you have a chance!

@conooi
conooi requested a review from parlough October 5, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Dark mode to Blog - Like dart version

3 participants