I’m posting this to invite discussion and feedback of CodeMirror 6’s CSS system. People have been having some difficulty with it, and I want to outline the constraints it exists under and describe a possible change in the design.
We’re not going to “just use” plain CSS files
Firstly, please keep any knee-jerk hostility to CSS-in-JS out of the thread. We have the following requirements:
-
Styles will be distributed over various packages. Users can not be expected to manually include the styles for all the packages they (transitively) load, and bundlers don’t (yet) provide a standardized way to bundle styles.
-
I’d really like to have proper isolation/scoping on CSS class names. Something like that is necessary for themes in any case. Being able to load multiple versions of the library without those interfering with each other is a plus.
Especially the first point just rules out plain old CSS files. And if we need to integrate this with the scripts in order to piggyback on JavaScript dependency management, we might as well use some of the options dynamic CSS provides us with.
That isn’t to say client code integrating CodeMirror can’t use plain CSS files. Making that possible is one of the goals. More about that in a moment.
The current system
What the library does right now is prefix the rules generated for themes and base themes with an additional class selector. For themes, that class is tied to the theme, and only added to the view’s outer node when the theme is active. For base themes (which are always supposed to be active, as a fallback base style), the prefixed class is shared between all the base themes and always added to the editor (but still generated to be unique for a given instance of the @codemirror/view
library).
“Theme selectors” are a concept used to normalize class names (always prefixing "cm-"
) and allow simple hierarchical relations between selectors: "panel.search"
expands to "cm-panel cm-panel-search"
, so that it inherits the styles from its parent selector but is itself more specific, so that rules for that selector override rules for the base class.
The predictable class names produced by theme selectors allow external style sheets to target editor elements.
This satisfies the requirements above, but has a few downsides:
-
The library generates rather specific rules, which are a bit of a pain to override in plain CSS. I.e.
.ͼ4.cm-light .cm-activeLine
(ͼ
indicates a generated class) has three class selectors in it, so, though the injected styles are put at the start of the page and thus can be overridden in rules defined elsewhere that have the same precedence, you do need to add an extra uninteresting class (for example.cm-wrap.cm-light .cm-activeLine
) to override them. -
To be able to add the prefix in the proper way, the library has to know when a rule includes a selector for the top element. I.e. for
$activeLine
(where$
indicates this is a theme selector), the prefix needs to have a space after it (.ͼ2 .cm-activeLine
) for$focused
(which is added to the top element when the editor is focused) there should be no space (.ͼ2.cm-focused
). This is confusing and the current solution (adding an extra$
to indicate you’re targeting the top element) is hideous. -
This kind of rule matching via classes on ancestors is a bit more expensive to do for the CSS engine compared to rules that just directly target a class.
Syntax highlighting
The highlighter uses a somewhat different system. Highlight styles define anonymous CSS classes for every tag they define. The highlighter emits the most specific matching style class (if any) for every bit of code.
Because the highlighting info is quite a bit more rich than what a typical highlighter will use, the editor does not currently emit fixed classes for all this information, because it would be a bit much. You’d get something like this for let x = [0];
:
<span class="cm-keyword cm-definitionKeyword">let</span>
<span class="cm-name cm-variableName cm-variableName-definition">x</span>
<span class="cm-operator cm-definitionOperator">=</span>
<span class="cm-punctuation cm-bracket cm-squareBracket">[</span>
<span class="cm-literal cm-number cm-integer">0</span>
<span class="cm-punctuation cm-bracket cm-squareBracket">]</span>
<span class="cm-punctuation cm-separator">;</span>
That seems excessive. Producing a somewhat lightweight DOM is one of my design goals. Hence the use of a different approach here. I do intend to add a way to create a highlighting style that just adds a (more limited) set of fixed classes for situations where you really want to use plain CSS to style code.
The disconnect between the way syntax (directly adding anonymous classes to targeted elements) is styled and the way the rest of the editor works (toggling a class on the outer element to enable rules targeting .ͼX .cm-something
) is a bit annoying.
Options
I’m considering two approaches to improve the situation. Feel free to propose other ideas if you think they might be helpful.
Minimalist
Mostly keep things as they are.
Replace the extra-$
syntax in themes with &
to make it slightly less confusing (&
for “parent selector” kind of has a precedent in the nested rule syntax).
Add a highlight style that styles tokens with fixed class names. Probably also add a way to apply multiple highlight styles at the same time.
Explain in the docs that to override styles generated by the scripts you’ll have to figure out how specific they are (easiest done in the devtools) and replace the ͼ class with cm-wrap
to keep the same specificity.
Back to directly applied classes
Up to about a year ago, themes worked differently. There was a method on the view that you’d pass a theme selector ID, and it’d return a string containing the class names that the active themes assigned to that selector. Code creating DOM elements would call this method to compute a class attribute for their elements.
The main downside of this approach was that it gets awkward to support dynamic theme changes—if there’s classes computed from the current theme all over the editor DOM, and all those classes have to be updated when the theme configuration changes. With a system that just toggles a class on some parent element, that problem doesn’t exist.
Still, this could be solved by just redrawing the entire view DOM when the theme changes (which isn’t that expensive either, and likely rare). View plugins would be torn down and reinitialized—if they are well-behaved plugins that keep their state in the editor state, this shouldn’t be disruptive.
(Somewhat related, I realized that there’s another situation that’ll require a solution like that anyway—currently, if you change the phrases
facet, your editor will not dynamically update its translateable text. It should. And the most practical way I can see to do that would be a redraw.)
In this system, theme definitions would work differently—every style provided by a theme would have to be scoped under some theme selector (say line
or panel
) so that its generated class could be added to those specific elements. You could still create more complex rules, like styling things differently when the editor is focused, using nested rules with &
notation. The $
notation could go away entirely.
In the current code, the only place where class names are directly embedded in the editor state (thus making it harder to update them when the theme changes) is in decorations. Adding direct support for specifying theme selectors to decorations, instead of requiring a literal class name to be provided, would make that problem easy to avoid.
An advantage of this approach would be that highlighting and theming become quite a lot more similar—both add class names to the relevant elements. You could say that code highlighting just doesn’t omit the stable classes by default as an optimization (but can be configured to emit them).