Skip to content

Match the surrounding page's theme automatically - #76

Draft
Auxeno wants to merge 3 commits into
google-deepmind:mainfrom
Auxeno:auto-theme
Draft

Auxeno wants to merge 3 commits into
google-deepmind:mainfrom
Auxeno:auto-theme

Conversation

@Auxeno

@Auxeno Auxeno commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

Related Issue: Feature request: Dark mode #7

Stacked on #75, that PR's commits are included in this diff; the new work is 6fd0c7a.

Part of #7, see that issue for screenshots.

Adds 'auto' to treescope.theme keeping 'light' mode as the default. Detection happens in the browser: the container reads the text colour it inherits from the page and checks whether it's light enough to have been meant for a dark background.

This is indirect, but works everywhere. prefers-color-scheme follows the browser rather than the notebook's own theme setting, and is reported incorrectly inside VSCode's output cells (microsoft/vscode#117804).

A rendering that has matched the page keeps that page's background rather than drawing its own, since themes disagree about which off-white or dark grey to use. Where JavaScript doesn't run there is nothing to match, so renderings fall back to light on a background of their own.

This changes the default for existing users, which makes it the most intrusive of the three changes. It's entirely reasonable to take the first two PRs and leave this one, or to keep 'auto' opt-in with 'light' remaining the default. Say the word and I'll adjust. Adjusted this.

Tests added for the detection hook and the attribute/dataset contract it depends on. pyink, pylint, pytype and the test suite are green.

@Auxeno Auxeno mentioned this pull request Aug 10, 2026

@danieldjohnson danieldjohnson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Interesting idea, I like the text-color-based autodetection! A few comments on the implementation and defaults.

Comment thread treescope/_internal/api/theme.py Outdated
module=__name__,
qualname="theme",
initial_value="light",
initial_value="auto",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think for now it would be best to keep the default as "light" to avoid unexpected behavior changes for users. We can still leave in "auto" as a setting so that users can customize this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure thing, happy to amend this.

const [r, g, b] = colorChannels.map(Number);
// Perceptual luminance, as in arrayviz's contrasting-color logic.
const pageIsDark = 0.2126 * r + 0.7152 * g + 0.0722 * b > 128;
root.dataset.treescopeTheme = pageIsDark ? "dark" : "light";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The name treescopeTheme is a bit confusing since it only seems to be used when the theme is auto? Maybe we can instead name this as root.dataset.treescopeDetectedTheme.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree, will rename, along with the matching data-treescope-detected-theme CSS selectors.

// inherited across the shadow root boundary, so this has to run before the
// rendering sets a color of its own. If we can't tell, we leave the attribute
// unset, and the rendering falls back to its own light background.
const inheritedColor = window.getComputedStyle(root).color;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think that window.getComputedStyle(root).color can resolve to a non-RGB color space in some cases (e.g. if the page specifies it as oklch(...)). It would be good if we could make this robust to that somehow.

One simple thing would just be to check that it's an RGB color and log an error otherwise. A possibly more advanced solution would be to use something like color-mix to force the browser to reinterpret the color as RGB?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, the regex would misread oklch/color() silently. Instead of parsing, we could potentially draw the colour onto a 1x1 canvas and read back the pixel, so the browser does the conversion for any colour format? If this fails, we could log a warning and fall back to light theme?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good idea, sounds reasonable!

Comment thread treescope/lowering.py Outdated
Comment on lines +377 to +381
:host([data-treescope-theme="light"]) .treescope_root.theme_auto {
/* Keep the page's own background, since themes disagree about which
off-white or dark gray to use. */
--treescope-bg: transparent;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you say more about the reasoning for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This will become much more readable with the rename from your second comment. Will update this to:

:host([data-treescope-detected-theme]) .treescope_root.theme_auto {
  --treescope-bg: transparent;
}

The idea behind these lines of code is if we have "auto" mode selected, and we're able to detect the notebook theme successfully, then we set the Treescope rendering canvas to be transparent, so the Treescope output gets drawn directly onto the cell output area, which feels more clean/seamless. Could alternatively try to derive the output cell area colour and use this for the render canvas, but this would be hard to do reliably.

In #7, (PR 3: Add an auto theme) I added a screenshot comparing "dark" to "auto" for reference :)

Renderings have always used dark text on a white background, which looks
out of place in a notebook with a dark theme. This adds a dark palette,
following VSCode's Dark Modern in the same way the existing light palette
follows Light+.

    treescope.theme.set_globally('dark')  # 'light' is the default
    with treescope.theme.set_scoped('dark'):
      ...

Block colors are unchanged, since they are chosen by the objects being
rendered rather than by Treescope.
`treescope.theme` now defaults to auto, which matches the page it is
rendered into. Detection happens in the browser: the container reads the
text color it inherits from the page and checks whether it is light
enough to have been meant for a dark background.

A rendering that has matched the page keeps that page's background rather
than drawing its own, since themes disagree about which off-white or dark
gray to use. Where JavaScript doesn't run there is nothing to match, so
renderings fall back to light on a background of their own.
- Keep light as the default, make auto opt-in.
- Rename detection attribute to data-treescope-detected-theme.
- Read the inherited text color via a canvas so any color format works. Failing logs a warning and falls back to light theme.
- Collaspse the transparent background selectors into one.
@Auxeno

Auxeno commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Have rebased this on #75 and applied the changes mentioned in the comments. Kept in one commit so it's easier to compare. Hope it's all right!

@danieldjohnson danieldjohnson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good, happy to merge once conflicts are resolved! (Seems like conflicts are just due to merging the prior PR)

// inherited across the shadow root boundary, so this has to run before the
// rendering sets a color of its own. If we can't tell, we leave the attribute
// unset, and the rendering falls back to its own light background.
const inheritedColor = window.getComputedStyle(root).color;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good idea, sounds reasonable!

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.

2 participants