Skip to navigation Skip to navigation Skip to content

Configuring dark mode

Learn how to configure dark mode and automatically apply it to your project.

Understanding modes #

Modes allow you to control common visual states that allow for both accessibility and comfort. For now, Hydrogen supports dark mode, but there are plans to also introduce a high contrast mode down the road.

Modes vs. themes #

Modes and themes are both ways of applying specific styles to a context. The biggest difference between the two is that modes are applied on a per-theme basis. This means that each theme you create in your configuration will have access to its own unique light and dark modes.

Applying modes to your project #

Hydrogen offers two ways to take advantage of mode-specific styles. By default, your configuration will have the method option set to preference, because this method doesn't require any intervention on your end to work. Alternatively, you can choose to take advantage of the more complex, but more useful toggle method. This option does require extra work to apply, but provides a much better user experience.

hydrogen.config.json

1
2
3
4
5
6
"modes": {
  "method": "preference"
  "dark": {
    ...
  }
}

The "preference" mode #

The preference-based approach relies on the prefers-color-scheme media query to enable styles when the user has set their browser to request a particular mode. These styles are automatic and will instantly apply when the end user sets their browser configuration. This option is great because it's low maintenance and doesn't require custom scripting or interactivity to work. The downside is that your application won't be able to be customized independently of the browser's setting, which can sometimes be frustrating for users who prefer a specific setting on individual applications.

The "toggle" mode #

The toggle-based approach is more complex and relies on data attributes to trigger styles. In order for it to work, you'll need to build a toggle UI element on your project that allows the user to make their theme choice. A basic working toggle can be found below.

The advantage to using toggle is that it allows your end user to choose between 3 options:

  • Their browser setting
  • Manual light mode
  • Manual dark mode
How to build a basic mode toggle #

Toggle-based dark mode requires an interface so that the user can make a selection based on their preference. An example of one in action can be found in the topbar area of this website.

At its core, the toggle should contain 3 button elements; one for each option. Using the onclick attribute, each button will trigger a script (outlined for you below) that enables the mode selected.

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ul 
  data-h2-list-style="base(none)"
  data-h2-display="base:children[>li](inline-block)">
  <li>
    <button onclick="enable_mode_preference()">
      Browser setting
    </button>
  </li>
  <li>
    <button onclick="enable_mode_light()">
      Light mode
    </button>
  </li>
  <li>
    <button onclick="enable_mode_dark()">
      Dark mode
    </button>
  </li>
</ul>

The first part of our script will be to grab the relevant Hydrogen elements from the DOM. We use querySelectorAll to grab all instances of data-h2 on your project just in case you're using multiple instances.

app.js

1
let instances = document.querySelectorAll('[data-h2]');

Now we can add the functions triggered by our toggle element. We use localStorage to store the user's choice so that it's selected by default the next time they return.

To start, the enable_mode_preference() will reset the mode settings to match the user's browser setting. In this case, we clear localStorage to ensure their browser preference is applied instead.

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Toggle preference
function enable_mode_preference() {
  if (window.matchMedia('(prefers-color-scheme: light)').matches) {
    instances.forEach((hydrogen) => {
      hydrogen.dataset.h2 =
        hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' light';
    });
  } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    instances.forEach((hydrogen) => {
      hydrogen.dataset.h2 =
        hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' dark';
    });
  } else {
    instances.forEach((hydrogen) => {
      hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '');
    });
  }
  localStorage.removeItem('mode');
}

Next up is the manual light mode toggle function, enable_mode_light(). When selected, this option will override the browser's setting and force the default light mode to appear. Don't forget to add their choice to localStorage.

app.js

1
2
3
4
5
6
7
// Toggle light
function enable_mode_light() {
  instances.forEach((hydrogen) => {
    hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' light';
  });
  localStorage.mode = 'light';
}

Finally, we have the manual dark mode toggle function, enable_mode_dark(). Just like the light toggle, when selected, this option will override the browser's setting and force dark mode to appear.

app.js

1
2
3
4
5
6
7
// Toggle dark
function enable_mode_dark() {
  instances.forEach((hydrogen) => {
    hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' dark';
  });
  localStorage.mode = 'dark';
}

The next part of our script will add an event listener to the window to help us detect when a user changes their mode setting. This part of the script only really applies if the user has selected the preference-based mode, but it will enable automatic switching between modes if the user changes their browser or system settings.

You'll notice that we also check for localStorage.mode here - this prevents the function from running if the user has manually chosen a fixed mode independently of their browser setting.

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Listeners
function watch_for_mode_changes() {
  if (
    (localStorage.mode && localStorage.mode === 'light') ||
    (!localStorage.mode && window.matchMedia('(prefers-color-scheme: light)').matches)
  ) {
    instances.forEach((hydrogen) => {
      hydrogen.dataset.h2 =
        hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' light';
    });
  } else if (
    (localStorage.mode && localStorage.mode === 'dark') ||
    (!localStorage.mode && window.matchMedia('(prefers-color-scheme: dark)').matches)
  ) {
    instances.forEach((hydrogen) => {
      hydrogen.dataset.h2 =
        hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' dark';
    });
  } else {
    instances.forEach((hydrogen) => {
      hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '');
    });
  }
}
window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => e.matches && watch_for_mode_changes());
window
  .matchMedia('(prefers-color-scheme: light)')
  .addEventListener('change', (e) => e.matches && watch_for_mode_changes());

Our final bit of code checks localStorage when the page loads to apply the correct mode. It's recommended that this script is placed in the head element of your html file so that it is applied as early as possible for a seamless transition.

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<head>
  ...
  <!-- Theme scripts -->
  <script>
    let hydrogen = document.querySelector('html');
    if (localStorage.mode) {
      if (localStorage.mode === 'light') {
        hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' light';
      } else if (localStorage.mode === 'dark') {
        hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' dark';
      } 
    } else if (
      window.matchMedia('(prefers-color-scheme: dark)').matches
    ) {
      hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '') + ' dark';
    } else {
      hydrogen.dataset.h2 = hydrogen.dataset.h2.replace(/dark/g, '').replace(/light/g, '');
    }
  </script>
  ...
</head>

Dark mode #

Because Hydrogen only supports dark mode for now, the dark object is the only available mode you can configure.

  • auto_apply_styles tells Hydrogen whether or not you want dark mode styles to apply automatically in your code. This setting allows you to define dark mode values for your theme and have them apply without any extra work. Disabling this setting allows you to apply your dark mode styles by hand, preventing unexpected results.
  • swap_default_modifiers will tell Hydrogen if you want it to automatically swap its default color modifiers for their opposite in dark mode. For example, if you set primary.darker as a color in light mode, this setting will automatically apply the primary.lighter value in dark mode.
hydrogen.config.json

1
2
3
4
5
6
"modes": {
  "dark": {
    "auto_apply_styles": true,
    "swap_default_modifiers": true,
  }
}

Dark mode styles are applied to elements using the :dark modifier on the base or a custom media query.

1
<span data-h2-color="base:dark(primary)"></span>

Automatic vs. manual styling #

Hydrogen provides the option to specify mode-specific values for theme configurations in your hydrogen.config.json file. Using these values, Hydrogen can handle the heavy-lifting of applying dark mode styles to your project for you. This functionality is further broken down into two control settings you can enable or disable to meet your needs.

Automatic dark mode #

The auto_apply_styles option will tell Hydrogen to automatically apply your dark mode specific values for you. This is a blanket application, so if you choose to tell the theme to swap out white for black in dark mode, every instance of white will automatically be black.

In a majority of cases, this is extremely helpful, because it will allow you to focus your effort on the handful of instances where the color swap doesn't make sense. This is particularly important when performing accessibility audits to ensure that your swapped color hasn't created contrast problems.

To extend this even further, a second control is provided through the swap_default_modifiers option that will automatically swap Hydrogen's default color modifiers for their opposite when dark mode is enabled. For example, primary.dark in light mode will swap to the value generated for primary.light when dark mode is toggled on. This allows for intuitive theme building by ensuring that a majority of contrast instances are accounted for when dark mode is automatically applied.

Manual overrides #

For times when the automatic swap isn't working as you'd expect, you can override automatic dark mode styles using the :dark modifier on your query. This is especially useful for accessibility corrections.

For objects that need to retain their styles across both light and dark modes, you can use the :all modifier on your query to force the styles to stay the same.

Styling dark mode manually #

If you'd rather not use Hydrogen's automated mode settings, you can still use the :dark query modifier in your attributes to apply styles that are unique to dark mode. This gives you full control over how dark mode styles work, but requires much more manual work to ensure that all of your elements and interfaces have been considered.