Skip to content

Follow system theme on change when respect-user-color-scheme: true#14552

Open
rmvegasm wants to merge 1 commit into
quarto-dev:mainfrom
rmvegasm:feature/follow-system-theme
Open

Follow system theme on change when respect-user-color-scheme: true#14552
rmvegasm wants to merge 1 commit into
quarto-dev:mainfrom
rmvegasm:feature/follow-system-theme

Conversation

@rmvegasm
Copy link
Copy Markdown

Problem

When respect-user-color-scheme: true is set in a Quarto HTML document, the
page respects the user's OS-level light/dark preference at load time, but does
not follow it when the OS theme changes while the page is open. This
behaviour is described in #12426 but I believe is no longer needed, and the
expected behaviour for most users setting respect-user-color-scheme: true
would be for the document to follow the system theme anytime it changes.

Root Cause

The matchMedia('(prefers-color-scheme: dark)') change listener in
quarto-html-before-body.ejs bails out early when any localStorage entry
exists:

queryPrefersDark.addEventListener("change", e => {
  if(window.localStorage.getItem("quarto-color-scheme") !== null)
    return;                       // ← bails forever after one manual toggle
  // ... apply system theme ...
});

Since every manual toggle writes to localStorage (via setStyleSentinel()),
one click permanently kills system-following.

Solution

Instead of returning early when a localStorage override is present, clear
the override
and follow the system. The manual choice is still respected
across page loads, but it expires gracefully on the next OS theme change.

queryPrefersDark.addEventListener("change", e => {
  const alternate = e.matches;
  if (window.localStorage.getItem("quarto-color-scheme") !== null) {
    window.localStorage.removeItem("quarto-color-scheme");   // override expires
  }
  toggleColorMode(alternate);
  localAlternateSentinel = alternate ? 'alternate' : 'default';
  toggleGiscusIfUsed(alternate, darkModeDefault);
});

This is the only change. The existing sentinel machinery
(hasAlternateSentinel, getColorSchemeSentinel, setStyleSentinel) works
unchanged: when localStorage is absent, it falls back to
localAlternateSentinel (which always reflects the current effective mode).

Behavior

Scenario Before After
Page load, system dark → light Follows Follows
User toggles once Override sticks forever Override sticks until next OS theme change
User toggles, then system changes Page ignores system Override expires, page follows system
User toggles, navigates, reloads Choice persists Choice persists (until next OS change)

Files Changed

  • src/resources/formats/html/templates/quarto-html-before-body.ejs
    (6 lines changed in the respectUserColorScheme event listener)

  • tests/integration/playwright/tests/html-dark-mode-defaultdark.spec.ts
    (updated the '...do not respect-user-color-scheme after toggling' test to
    reflect the new behaviour: renamed to '...manual override expires on system theme change', assertions now expect the page to follow the system after a
    manual override expires, rather than sticking forever)

    All 14 existing tests in the dark-mode and light-mode suites continue to
    pass with these changes.

Fixes #14551

@posit-snyk-bot
Copy link
Copy Markdown
Collaborator

posit-snyk-bot commented May 27, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

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.

respect-user-color-scheme: true does not follow system theme dynamically

2 participants