Styling and theming design discussion

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).

2 Likes

I tried reading this post in full twice and I’m still having some difficulty understanding it :joy:. That is not to say anything you wrote was unclear; this is just a very difficult challenge with many complex trade-offs.

I suspect many others after reading this post will feel similarly powerless to contribute, given the rarity of developers at @marijn 's level (I know I’m not…), with sufficient amount of time to deep dive into CM6’s internals.

Perhaps to help others participate, it would make things easier if there are some concise and summarized points, ideally phrased from the user’s perspective. (By user I mean developer who will be using CM6 in their projects)


After testing the waters with cm6 (and deciding to go ahead actually using it in production), here’s how I see the problem: I think the theme system and the highlighting system are two sets of challenges, while with some overlap, but both have possibly different design goals.

Theme system

Honestly I’m ok with the way it is right now. It’s certainly annoying and cumbersome at times to write more specific rules to override the base theme set by cm6, but it’s still doable.

I can’t think of a better way to provide isolation between different editors on the same page. In the old days of CM5 it’d just be adding a custom class to the editor anyway.

Syntax highlighting

I think this is where people ran into most trouble. The lack of a persistent class name made some things (like debugging language parsers) difficult, and others (using css files to style syntax highlighting) practically impossible.

I understand this opens up the possibility of isolation between editors, but I think it’s still possible to get this benefit with fixed class names by prepending the randomly generated class from the wrapper.

Personally, I’ve had to make a fork of the language parser to generate tokens with a “css class” attribute rather than a Tag attribute, and I’ve then forked the highlighter plugin to make use of that css class attribute. This worked well for us, but unfortunately isn’t a great idea in the long term for maintainability.

I still prefer the old style “by string convention” where it was rather easy to add new strings to both a language parser, and the highlighter theme. However, I do understand that given CodeMirror is primarily targeted at code editing, having custom token types (highlighting wise) is probably not a design goal.

Thanks for responding.

This should be addressed by the proposed feature of a highlight style that emits stable classes (which will be done regardless of which approach I take here).

This is still not hard—export your custom Tag instances for your custom tokens from your language package, and target them your highlighter.

1 Like

Sounds great!

This should be easier once the stable classes are available. Was there any change recently to support line classes? I’m doing something similar from the highlighter plugin.

See 0.18.0 for a summary of what I ended up doing. It should address some of these concerns.

1 Like

On .ͼ4.cm-light .cm-activeLine vs .ͼ2 .cm-activeLine:
What if you added an extra wrapper whose sole purpose is getting the generated class, and no other classes? If doable, you’d get a consistent .ͼ4 .cm-light .cm-activeLine vs .ͼ2 .cm-activeLine.
[I’m not following the whole picture and might be missing reasons against this…]

Thanks for the suggestion. I considered that, but wasn’t comfortable with that additional bit of spurious DOM complexity, especially since people will sometimes want to run querySelector and similar on the actual outer element of the editor, and having a separate class for that seems annoying.