Skip to content

feat(docs-infra): add dark mode to documentation web site#40257

Closed
shane99a wants to merge 4 commits into
angular:masterfrom
shane99a:dark-mode
Closed

feat(docs-infra): add dark mode to documentation web site#40257
shane99a wants to merge 4 commits into
angular:masterfrom
shane99a:dark-mode

Conversation

@shane99a

@shane99a shane99a commented Dec 24, 2020

Copy link
Copy Markdown

Add dark mode to angular.io web site to make it easier to read

Add a button to the top right of the web site to toggle dark mode
Save user selected theme using local storage
Use theme picker component source code from angular material with some modifications

Do not run dark mode when printing is enabled

Modify the current styles spead accross many files by placing the relevant styles into one file for dark mode

Closes #40102

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.io application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

Web site currently has no dark mode

Issue Number: #40102

What is the new behavior?

Adds dark mode and ability to toggle it

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Please try it out as well locally to see if there's something missing.
image

Add dark mode to angular.io web site to make it easier to read

Add a button to the top right of the web site to toggle dark mode
Save user selected theme using local storage
Use theme picker component source code from angular material with some modifications

Do not run dark mode when printing is enabled

Modify the current styles spead accross many files by placing the relevant styles into one file for dark mode

Closes angular#40102
@google-cla google-cla Bot added the cla: yes label Dec 24, 2020
@pullapprove pullapprove Bot requested a review from gkalpak December 24, 2020 04:12
@ngbot ngbot Bot added this to the Backlog milestone Dec 24, 2020
@mary-poppins

Copy link
Copy Markdown

You can preview a4e170a at https://pr40257-a4e170a.ngbuilds.io/.

    Moved hardcoded colors to variables

    Fixed color issue for table of contents on mobile

    Fixed edit icon color issue for github links

    Closes angular#40102
@mary-poppins

Copy link
Copy Markdown

You can preview 788a2a9 at https://pr40257-788a2a9.ngbuilds.io/.

@petebacondarwin

Copy link
Copy Markdown
Contributor

The colours on the API list page need some work. When not hovered over the contrast of list items seems too low, and when you do hover we get an ugly white box.

Screenshot 2021-01-06 at 14 44 57

@petebacondarwin petebacondarwin added the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Jan 6, 2021
@petebacondarwin

Copy link
Copy Markdown
Contributor

I think it would be wise to get some buy-in from @IgorMinar and @aikidave on this before you spend much more time on it.

@kapunahelewong

Copy link
Copy Markdown
Contributor

Reviewing per @aikidave's request.

@shane99a thank you so much for doing this. I imagine you are still working out all of the details, but I'm including the things I noticed here in case you iterate further.

What's your reasoning for switching from blue to green for the toolbar?

In terms of contrast, there are a couple of places that could use tweaking. As @petebacondarwin pointed out, the API page contrast is low. We would need to brighten that font color to get it to pass the WebAIM contrast checker.

Tables and alerts would also need some contrast either in their background or border (something to differentiate those elements). In light mode they're set off by a box shadow, but this gets lost with dark colors. Maybe a bright border (#1976d2)? Here's a table from the cheat sheet.

Screen Shot 2021-01-06 at 12 45 28 PM

This next image is from the style guide. We would want to make sure that code snippets stand out consistently (the red really stands out on the dark background, but the regular ones fade away a bit).

Screen Shot 2021-01-06 at 11 26 10 AM

<code-example> would need contrast too in the header or border.
Screen Shot 2021-01-06 at 11 25 51 AM

I love dark mode and really like where you're going with this. If our leads support this, I'm happy to help move it forward in any way I can.

@IgorMinar

Copy link
Copy Markdown
Contributor

This looks like a really good feature! Thank you for proposing it. If we can sort out all of the accessibility/contrast issues then I'd love for us to land this change.

Added style tweaks based on feedback and what looks right

Fixed styles that were missed before

Closes angular#40102
@mary-poppins

Copy link
Copy Markdown

You can preview fdce7a6 at https://pr40257-fdce7a6.ngbuilds.io/.

Added elevation to table to make it stand out
Tweaked search results page for dark mode
Fixed styles that were missed before

Closes angular#40102
@mary-poppins

Copy link
Copy Markdown

You can preview f41ac5c at https://pr40257-f41ac5c.ngbuilds.io/.

@IgorMinar

Copy link
Copy Markdown
Contributor

@jelbourn can you please help review this one? I believe you were involved in building some other dark themes for material apps.

@IgorMinar IgorMinar requested a review from twerske January 13, 2021 21:58
@IgorMinar

Copy link
Copy Markdown
Contributor

@twerske can you please review this PR for visual appeal and accessibility issues? I spoke with Minko and he suggested that you'd be a good person from devrel to take a look. Can you please manually review the main pages as well as sample of content under api and guides sections? thank you!

@twerske twerske 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.

Reviewing focused on a11y per @mgechev's request.

Thank you for working on this - yay for dark mode!

I've outlined a few values that need changes to meet contrast guidelines. I'd recommend using an accessibility scanner like: https://www.deque.com/axe/ to ensure all dark-mode specific contrast changes meet WCAG. I'd aim for AAA as a best practice since a majority of this text is small (especially on devices or mobile).

Some overall changes that need to be addressed are looking at text selection, hover status and highlighting. The blue accent color on links combats a bit with the blue text selection:
Screen Shot 2021-01-13 at 2 31 47 PM
Screen Shot 2021-01-13 at 2 34 58 PM

$ng-io-theme: mat-light-theme($ng-io-primary, $ng-io-accent, $ng-io-warn);

$night-primary: mat-palette($mat-blue, 800, 700, 900);
$night-accent: mat-palette($mat-blue, 300, 200, 400);

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.

Suggested change
$night-accent: mat-palette($mat-blue, 300, 200, 400);
$night-accent: mat-palette($mat-blue, 200, 100, 400);


.is-helpful {
&.alert, &.callout {
background-color: $theme-hover-background-color;

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.

In this use case, this callout/alert section looses it's visual impact - I'd test a higher contrast value for $theme-hover-background-color such as black (going darker increases the contrast of the light text on top).

Screen Shot 2021-01-13 at 2 22 37 PM

}

.card-footer {
color: $theme-accent-color;

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.

This is an example of the contrast being lost on hover - if the ratio can meet WCAG AAA (7.0 contrast) that'd meet best practices for our small text.
Screen Shot 2021-01-13 at 2 38 27 PM

color: $theme-foreground-color;

&:hover {
background-color: $theme-hover-background-color;

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.

Spot check contrast here since this is the primary site navigation - Screen Shot 2021-01-13 at 2 39 50 PM

@jelbourn jelbourn 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.

This is my first batch of comments. I will have a second batch on the Sass overrides file.

@@ -0,0 +1,3 @@
<button mat-icon-button aria-label="Theme switcher button" (click)="currentTheme.name === 'night-theme' ? selectTheme('ng-io-theme') : selectTheme('night-theme')">

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.

I think the aria-label here should be either "Switch to light mode" or "Switch to dark mode" (based on the active theme).

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.

Since the icon also conveys valuable information of if the site is currently in light or dark mode, I'd recommend adding "sun icon" and "moon icon" to aria-labels



describe('ThemePicker', () => {
let themeStorageService: any;

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.

Rather than using any, it would be good to match the interface of the actual class here

changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class ThemePickerComponent {

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.

Just ThemePicker should be sufficient

encapsulation: ViewEncapsulation.None
})
export class ThemePickerComponent {
readonly themes: DocsSiteTheme[] = [

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.

I don't think this theme data structure is necessary for angular.io. On the material site, we need these fields for additional behavior. For example, we need a display name because we show the options in a dropdown menu, which is not necessary here.

I think this can be simplified down to a single string value, theme: 'light' | 'dark, with the icon based on that string value. I would also generate the applied CSS class based on that value like `${activeTheme}-theme`

];

readonly defaultTheme: DocsSiteTheme = this.themes[0];
currentTheme: DocsSiteTheme = this.themes[0];

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.

This is more a style preference, but I would use activeTheme over currentTheme

}
}

selectTheme(themeName: string) {

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.

Since this is only doing light/dark, I would make this toggleTheme, dropping the parameter.


@Injectable()
export class ThemeStorageService {
static storageKey = 'docs-theme-storage-current-name';

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.

Maybe just 'aio-theme'



@Injectable()
export class ThemeStorageService {

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.

I would just do ThemeStorage

}
}

clearStorage() {

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.

This method doesn't appear to ever be called in the app so it can probably be dropped

Comment on lines +32 to +37
@media not print {
.night-theme {
@include angular-material-color($night-theme);
@include custom-styles($night-theme);
}
}

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.

This is going to include all of the CSS for light and dark themes at the same time, which we don't particularly want. On the material site, we swap out the href on the link to point to a different css file based on the theme preference so that we only load one theme at a time.

One concern with this approach is that, on slower devices, you can sometimes see a flash of the default theme before your preferred theme is loaded, since that isn't resolved until Angular has bootstrapped.

@shane99a shane99a Jan 14, 2021

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

If you go network panels and slow it down to 3g for https://material.angular.io. You'll notice the same issue happening.

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.

Yeah, it's something we decided we were okay with on the material site, but other folks would have to weigh in on angular.io. If you didn't have the saved preference, you could use the media attribute of the link element to target prefers-color-scheme. But with the preference, you'd need a small snippet to read/set the href before anything else happened.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

If the sass is all compiled to a single css file like it is here, then I don't know if you would notice the change in styling. If anything you would notice it if you tried the approach of href and dynamically loading another css file (as I have ran into that issue in other projects). I'm trying to see if slowing it to 3g does cause what you talked about, but I couldn't reproduce it.
Media attribute is a bit more risky as I wanted the user to have the option to opt out if they don't want it.

@jelbourn jelbourn Jan 15, 2021

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.

The issue with using CSS classes to toggle the theme isn't that you'll get a flash of the default theme, it's that the browser has to download a nontrivial amount of additional CSS that they aren't using. I don't know the total size offhand, but we do our best to keep the payload size of angular.io as small as we can, and doubling the theme styles would be something we'd want to avoid.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah if the concern is size, then my PR is not gonna meet the standard. It's meant to not infringe the current code as much as possible without affecting the feature. Feel free to close it, and maybe someone can tackle the refactoring at some point.

@@ -0,0 +1,598 @@
@mixin custom-styles($theme) {

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.

Looking through this file, I don't think we can move forward with it in its current form. The biggest problem is that it's relying on global styles to override component-specific styles with greater specificity. These global overrides, though, are disconnected from the code they're affecting. This disconnect will make any maintenance going forward exceptionally difficult. This is compounded by the fact that this single file has implicit knowledge of every hard-coded color style in the app. Another side-effect here is that it becomes very easy to introduce a style into this file that accidentally affects more than it intends, since anything in this file affects the entire app all at once.

Another issue is that the app ends up delivering the hard-coded and the dark-mode overrides. The end result is that we're delivering more CSS than really necessary. This also has the side-effect of making the code harder to debug in devtools since there's more going on with any particular element.

Another issue is that these overrides seem to target the internals of Angular Material components in many cases, which is rather brittle since it's tied into the exact DOM structure of the component today. All of the Angular Material components support dark themes, so I'm guess the problem here is that those styles are already being overridden elsewhere in the app here without doing through their theme mixns?

There are a few general approaches we could go about resolving these issues. The first is what Angular Material does: splitting color styles for the app into separate Sass files, and combining those into a theme CSS file, which can then be dynamically loaded/swapped. I suspect this would be a large-ish undertaking for aio since you'd have to do a nontrivial refactoring for every component in the app.

The second approach would be to eliminate the hard-coded colors across the app and replace them with a fixed set of theme CSS classes derived from the Sass theme. For example, if a component current has something like

.content-projection-example {
  color: darkgray;
}

You would eliminate this style and instead apply a predefined CSS class like .theme-foreground-color to the element in the template. This class would be defined in the theme CSS file, which would be dynamically loaded based on the light/dark preference, something like

.theme-foreground-color {
  color: mat-color($foreground-palette, text);
}

I think this would also be a large-ish undertaking that touches a lot of the app.

The third approach would be to use CSS Variables, redefining the hard-coded colors within each component to be based on a limited set CSS Variables, which are in turn based off values coming from the theme in Sass. The blocker here, though, is that CSS Variables aren't supported in IE11, which we still support for Angular. This will become a much more attractive option in the future, but for now I don't think we can feasibly adopt it. (as a side note, you unfortunately wouldn't be able to use Sass variables to implement this approach due to the way Angular bundles component CSS into JavaScript).

At the very least I think @gkalpak @IgorMinar and @mgechev would want to weigh in on this. I'm biased, but I think that using the same approach as Angular Material is the "best" end result here, though it's likely a fair amount of work to get there.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah I understand what you mean. I decided to go with this approach, as I am not sure if that level of refactoring would bring that much value relative to the feature itself, especially with this being my first time touching this repo.
What about a fourth option to make all the corresponding sass files take in the theme input, similar to how custom_styles does it? Would that work? Probably the only other way I can think of that reduces significant refactoring.

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.

That unfortunately would be the same as trying to use Sass variables, which I mentioned in the block for the third approach. The problem here is that Angular bundles a component's CSS into its JS output. This lets the styles participate in the JS module system (i.e., you only get the styles for a component when you use it), but also means that you can't swap out the style at runtime.

@@ -0,0 +1,598 @@
@mixin custom-styles($theme) {

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.

This @mixin file is quite a bit of custom overriding of the current dark theming included in Material

There should be a cleaner way to use material theming and not need this amount of overriding - making use of things like defining theme scss variables: $mat-dark-theme-background and $mat-dark-theme-foreground

@jelbourn

Copy link
Copy Markdown
Contributor

Ack, sorry we weren't able to move forward with this. I do want this feature a ton- it's a running joke on the team how much I want a dark mode for everything. I hope to take a pass at it when there's an opening in the roadmap.

@jelbourn jelbourn closed this Jan 20, 2021
@AleksanderBodurri

Copy link
Copy Markdown
Member

I'm really sad to see this PR close. @jelbourn with your permission I'd like to attempt this. I'm familiar with how https://material.angular.io/ handles theming and I threw together a quick example branch porting over similar functionality here for one component style. This branch has the theming infrastructure from material and the theming created for the background-sky css class within the marketing-layout styles (see demo video). The demo is toggling between the default blue theme and a red theme but this could be changed to work with a light/dark theme.

In particular the implementation differs from this PR by:

  • Lazy loading the theme css. This is less important in this case than it is for material because there are only 2 themes and the default theme 'light' will always have its css loaded. Similar to how the indigo-pink theme for material always has its css loaded even if a different theme is selected. Still, this means that if a user has the "light" theme active then they won't download the dark mode css.
  • Splitting color styles out into theme scss files, addressing the maintainability concern.

See master...AleksanderBodurri:aio-theming

Demo:

angular-io-theming.mov

Refactoring out all the styles would be a non-trivial undertaking but I think it would be worth it. It would open up the door for more themes in the future and could lead to other optimizations like lazy loading the archive, release candidate, and next version css for old/rc/next versions of the angular docs, instead of including them in the stable release version of the docs.

@twerske

twerske commented Mar 8, 2021

Copy link
Copy Markdown
Contributor

I'm just now seeing this @AleksanderBodurri -- I'd love to see you take this on!

I'm tagging @jelbourn to see what he thinks about this approach, the general infra of lazy loaded theme looks good though.

@jelbourn

jelbourn commented Mar 8, 2021

Copy link
Copy Markdown
Contributor

Yeah, I somehow missed the notification on this one. If you're up to send a PR that

  1. Splits out the app's color styles into separate theme mixins, and
  2. Lazy-loads the light dark theme

Then I'd be happy to take a look

@AleksanderBodurri

Copy link
Copy Markdown
Member

@jelbourn I've opened up a draft PR with an initial implementation #41129

@angular-automatic-lock-bot

Copy link
Copy Markdown

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot Bot locked and limited conversation to collaborators Apr 9, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews cla: yes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add dark mode to angular.io

9 participants