Would love feedback from contributors (and any other users): @janosh @codepunkt @JCQuintas @chivandikwa
Background
gatsby-remark-vscode currently supports multiple themes activated by prefers-color-scheme
media queries. Many users’ sites, however, have a manual theme selection switch, which typically works by reading the system theme preference via matchMedia, using that as the default state of the switch, then writing a class name to the html
or body
element reflecting the current theme selection, so the system preference can be overridden by interacting with a switch. (Example implementation: donavon/use-dark-mode.) In these cases, theme-specific CSS is usually written with respect to that class name and prefers-color-scheme
media queries are omitted entirely. gatsby-remark-vscode has poor support for this convention. #39 tracks this feature request, and #55 is a working prototype of it by @codepunkt.
Additionally, discussion in #55 made me aware of a subtle bug with HTML and CSS generation in existing multi-theme support where token styles can leak from the default theme into a media-query-activated theme, and fixing it will require fairly significant changes in how token class names are generated.
Problem(s)
I was having trouble figuring out an API for #39 that struck a balance between simple and flexible without introducing breaking changes. Given that the aforementioned newly discovered bug will require changes that could potentially break users relying on the current token class name generation strategy, I think it might be time to embrace breaking changes and consider releasing a new major version. With the freedom that affords us to change things, I would like to:
- fix the aforementioned token class name generation bug
- allow users to choose an arbitrary number of themes to be selected by a parent selector (#39, but more flexible)
- future-proof the API against new values for
prefers-color-scheme
and other potentially relevant media queries in the draft of Media Queries Level 5
- reduce the confusion around the difference between
defaultTheme
and prefersLightTheme
in the current API
Proposal
New theme
options API
To address problems 2), 3), and 4) above, I propose to introduce a new top-level options key called theme
, and deprecate colorTheme
. Use of colorTheme
in v2 will continue to work, but will issue a deprecation warning. Examples usage of the new option:
Single-theme
{
theme: 'Default Dark+'
}
Parent selector theme selection with light, dark, and high-contrast
I anticipate this to be the most common configuration used for anyone using a dark mode switch.
{
theme: {
default: 'Solarized Light',
parentSelector: {
'html.dark': 'Default Dark+',
'html.high-contrast': 'Some High Contrast Theme'
}
}
}
Advanced media query theme selection with light, dark, high-contrast, and print-optimized
{
theme: {
default: 'Default Light+',
media: [{
match: 'screen and (prefers-color-scheme: dark)',
theme: 'Default Dark+'
}, {
match: 'screen and (prefers-contrast: high)',
theme: 'Some High Contrast Theme'
}, {
match: 'print',
theme: 'Some Printer-Friendly Theme'
}]
}
}
Shorthand preset for prefers-color-scheme: dark
{
theme: {
default: 'Solarized Light',
dark: 'Oceanic Next'
}
}
Parent selector theme selection with media query fallback
The parent selector strategy has naturally higher CSS precedence than the media query strategy, but it might be desirable to use media queries as a fallback while JavaScript is loading, or if JS fails to execute for some reason.
{
theme: {
default: 'Solarized Light',
parentSelector: {
'html.light': 'Solarized Light',
'html.dark': 'Default Dark+',
},
media: [{
match: '(prefers-color-scheme: dark)',
theme: 'Default Dark+'
}]
}
}
Callback option value
This will work the same as it does today with colorTheme
.
{
theme: ({ language }) => {
// Only enable dark theme for JavaScript
if (language === 'js') {
return {
default: 'Default Light+',
parentSelector: {
'.dark': 'Default Dark+',
'.hc': 'High Contrast'
}
};
}
return 'Default Light+';
}
}
New class name generation
To explain the motivation for this, I have to explain the leaking token styles bug mentioned earlier. Imagine a config where the default theme has lots of different token colors, and then a media-query-activated theme has only one color:
{
theme: {
default: 'Default Dark+',
dark: 'Monotone Purple'
}
}
The way tokenization works is that the code is split into tokens with different scopes based on the TextMate grammar for whatever language of code is being analyzed. These scopes are independent of theme. As an example, the keyword const
in JavaScript has the scopes storage.type.js
, meta.var.expr.js
, and source.js
(and may have more in other contexts). Then, TextMate themes map these scopes to colors. The tokenizer keeps track of how many unique colors a theme contains and maps these to sequentially numbered class names in the form of mkt0
, mkt1
, and so on.
So, even though the keyword const
has the same scopes regardless of theme, it will almost certainly receive a different class name depending on which theme is set during tokenization, because “Monotone Purple” will label every token as mkt0
. The current bug is that I only tokenize once, with the last theme set. In this config example, “Monotone Purple” would be activated during tokenization, which means every token would have the class name mkt0
, so the “Default Dark+” theme will also be monotone, with whatever color it associates with mkt0
.
To fix this, we need to run the tokenizer once per theme (or at least translate scopes to class names once per theme), and assign theme-specific class names to tokens when themes produce different class names, so our const
token becomes something like
- <span class="mkt0">const</span>
+ <span class="default-dark-mkt8 monotone-purple-mkt0">const</span>
And of course, the would have to update with it. Eventually, this substantially increases the weight of the CSS and HTML generated. The current global class names are also not terribly principled, so I want to adopt a grvsc
prefix (for Gatsby-Remark-VSCode), and use either a hash or an index in place of serializing theme names:
- .default-dark .default-dark-mkt8 { color: #efefef; }
- .monotone-purple .monotone-purple-mkt0 { color: purple; }
+ .grvsc-t0 .grvsc-t0-8 { color: #efefef; }
+ .grvsc-t1 .grvsc-t1-0 { color: purple; }
- <span class="default-dark-mkt8 monotone-purple-mkt0">const</span>
+ <span class="grvsc-t0-8 grvsc-t1-0">const</span>
(I’d continue to put a more human-readable theme class name on the pre
tag for consumers to target, but internal class names can be mangled for brevity.)
I’ll likely take this opportunity to add similarly prefixed class names and CSS variables as aliases for existing ones (e.g. .vscode-highlight-line
) for backward compatibility for the duration of v2, perhaps with a flag to drop the old ones.
Notable restrictions
- While a user can choose to employ both a parent selector strategy and a media query strategy, the parent selector will always have higher precedence. It’s intentionally not possible to activate a theme by the compound criteria of a matching media query and a parent selector. It’s also not possible increase the specificity of a media query theme such that it would win over a parent selector theme. Although the API leaves room for that as a future possibility, my current thinking is that any more complex media query logic can be implemented by the user with custom JavaScript by setting parent selectors in response to
matchMedia
.
- Since it’s possible for multiple media queries to match at the same time, the user will have to take some care to order the
media
array appropriately. The last theme in the array will have the highest precedence since it will be written to the stylesheet last.