Foundations

Dark mode

Dark mode is built into every Puppertino component. Colors flip automatically through CSS custom properties, surfaces lighten as they elevate, and shadows quiet down. There are two ways to opt in: automatic detection from the operating system, or manual toggling controlled by your code.

Automatic mode

The simplest setup. Add p-auto-dark-mode to the body and Puppertino flips colors when the user’s system preference is dark.

<body class="p-auto-dark-mode">
  <h1>Hello</h1>
</body>

This uses the prefers-color-scheme: dark media query under the hood. No JavaScript required. The page reflows instantly when the user changes their system preference, even while the tab is open.

Use this when you do not want a manual toggle and you trust the system preference.

Manual mode

Add the p-dark-mode class to the body when the user opts in. Remove it when they opt out. The class can be added by your code, by a button click, or by reading from local storage on page load.

<body class="p-dark-mode">
  <h1>Hello in dark mode</h1>
</body>

Use this when you want to give people a manual toggle that overrides their system preference.

The dark mode manager

Puppertino ships a small JavaScript helper that handles the manual toggle workflow: persistence, system detection, cross-tab sync, and the toggle action itself. Include it once and call init().

<script src="https://cdn.jsdelivr.net/npm/@codedgar/puppertino@2.0.0/src/js/darkmode_manager.js"></script>
<script>
  puppertinoThemeMan.init({
    autoDetect: true,
    darkThemeClass: 'p-dark-mode'
  });
</script>

Options

OptionDefaultWhat it does
autoDetecttrueFalls back to the system preference when the user has no saved choice
darkThemeClass'p-dark-mode'The class name applied to <body> when dark mode is active

Toggling

Call puppertinoThemeMan.toggle() from a button click. The manager adds or removes the dark mode class and saves the choice in localStorage.

<button id="theme-toggle">Toggle theme</button>

<script>
  document.getElementById('theme-toggle').addEventListener('click', () => {
    puppertinoThemeMan.toggle();
  });
</script>

Reading the current theme

Use isDarkThemeActive() to check whether dark mode is currently applied. Useful when your code needs to branch on the theme, like picking which chart palette to load.

if (puppertinoThemeMan.isDarkThemeActive()) {
  loadDarkChartPalette();
} else {
  loadLightChartPalette();
}

If you only need to change how something looks, you do not need JavaScript. See Styling your own CSS below.

Cross-tab sync

When the user toggles dark mode in one tab, every other open tab in the same browser updates immediately. The manager listens to the storage event from localStorage and applies the new theme without a reload. No configuration needed.

Styling your own CSS

Manual theming is a class, so your own CSS can follow it with a selector. No script has to read the theme first.

When the class is on <body>, anything inside it is a descendant. Write the dark variant of your rule under .p-dark-mode.

.site-logo {
  background-image: url('logo-light.svg');
}

.p-dark-mode .site-logo {
  background-image: url('logo-dark.svg');
}

When the class sits somewhere else, use :has(). It lets a rule react to the class from outside it, which covers two cases the descendant selector cannot: styling <body> itself, and styling elements that are not inside the dark region.

/* The class is on a wrapper, and the page behind it should follow */
body:has(.p-dark-mode) {
  background: #000000;
}

/* A header that sits outside the themed wrapper */
body:has(.p-dark-mode) .site-header {
  border-color: rgba(255, 255, 255, 0.1);
}

:has() works in every current browser.

For automatic mode there is no class to hook into when the system flips, so pair the class with the media query.

@media (prefers-color-scheme: dark) {
  .p-auto-dark-mode .site-logo {
    background-image: url('logo-dark.svg');
  }
}

Consider reaching for the tokens first. A rule that reads var(--p-text-primary) or var(--p-bg-primary) already follows both modes and needs none of the selectors above. Keep the selectors for what tokens cannot express, like swapping an image.

How it works under the hood

Puppertino’s color system is built on CSS custom properties. The dark mode class flips a single set of variables, and every component that uses those variables updates in one paint.

:root {
  --p-text-primary: rgba(0, 0, 0, 0.85);
  --p-bg-primary: #FFFFFF;
}

.p-dark-mode {
  --p-text-primary: rgba(255, 255, 255, 0.85);
  --p-bg-primary: #000000;
}

This is why dark mode is instant. There is no per-component override, no JavaScript repaint loop, no flicker. The browser swaps the variables and recalculates styles in one frame.

You can use the same approach for your own colors. Define them as variables on :root, then override them under .p-dark-mode for the dark variant. Puppertino’s existing components will pick up your custom variables for free if you use the same naming convention.

What flips in dark mode

Every token from the Colors page flips. Specifically:

The Materials page demonstrates the material flip side by side over the same image.

Best practices

Consider supporting both auto and manual modes. Auto handles the common case. Manual lets people override.

Avoid hard-coding colors. Use the Puppertino tokens or define your own custom properties. Anything hard-coded will not flip with the theme.

Avoid relying on shadow alone for elevation in dark mode. Shadows are less visible against dark backgrounds. Apple compensates by raising the surface color. Puppertino’s material system handles this automatically.

Test your designs in both modes. A layout that works in light may have legibility issues in dark, and vice versa. The most common pitfall is text contrast over translucent surfaces.

Backwards compatibility

The .p-dark-mode class and the dark mode manager have been part of Puppertino since v1. Existing projects continue to work without changes. The v2 color tokens (text hierarchy, fills, materials) flip automatically.

Copied