Dynamic light mode / dark mode - how?

In the process of climbing the 5 → 6 hill, and while I’ve managed solutions so far for most things, the CSS transition has me a bit stumped, hoping someone might point me in the right direction.

Our usage of CM is predominantly on mobile devices, and a significant percentage of users use dynamic light mode / dark mode on the device, where it’ll switch based on time of day. Previously, handling this was quite straightforward; we’d simply define alternate colors for the relevant classes within a prefers-color-scheme block, in our SCSS e.g.,

@include prefers-color-scheme($color-scheme-alt) {
  .CodeMirror.cm-s-default {
    background-color: #212121;
    color: #EEFFFF;

   ...etc
  }
}

And that worked well; when the device auto-switched, the relevant color scheme would be automatically applied by the browser.

I’m struggling to understand how one handles this type of thing in v6’s themes and syntax highlighting.

I was able to dynamically change the theme between light and dark by creating both themes using the example here: theme-one-dark/one-dark.ts at main · codemirror/theme-one-dark (github.com)

I ended up having a light theme created on my project and the dark theme imported from the npm package @codemirror/theme-one-dark.

The important piece here is the Compartment, the following example is with Angular:

import { json } from '@codemirror/lang-json';
import { foldGutter } from '@codemirror/language';
import { closeSearchPanel, openSearchPanel, search } from '@codemirror/search';
import { Compartment, EditorState } from '@codemirror/state';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, lineNumbers } from '@codemirror/view';
import { oneLight } from './editor-panel-theme.model';

//Inside the class
editorTheme = new Compartment();


//In my AfterViewInit method
this.view = new EditorView({
            state: EditorState.create({
                extensions: [
                    lineNumbers(),
                    foldGutter({
                        closedText: '▶',
                        openText: '▼'
                    }),
                    EditorState.readOnly.of(true),
                    search(),
                    this.editorTheme.of(oneLight),    //here is where we use the Compartment
                    json()
                ],
                doc: self.text
            }),
            parent: this.jsonViewer.nativeElement
        });

//In your event just reconfigure
this.view.dispatch({
                    effects: this.editorTheme.reconfigure(
                        selectedTheme === "light" ? oneLight : oneDark
                    )
                });

4 Likes

Thanks for the response; very helpful. It looks like in every browser we care about, there’s the ability to listen for the media change:

So in theory, at least, we should be able to do something like:

window.matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', event => {
  if (event.matches) {
    //dark mode
  } else {
    //light mode
  }
})

And thus know when the device has auto-switched modes and it’s time to reconfigure. Will give this a try.

And, as a followup, this seems like a lot of plumbing to do what CSS gave me for free, so I’m curious if I’m just missing something. It seems as if to an extent I could just use CSS as before for the chrome portions, but the highlight portion appears to be somewhat hostile to that approach.

Following up on my own investigation here; we’ve arrived at a workable solution for our use case:

  1. I suspect that many here use CM as the main event, so to speak, and thus want the ability to define the look and feel as one would a code editor that would be in use for 8 hours a day.
  2. Our usage is quite different; it’s a small ancillary thing, used infrequently, so we just want it to look as an integrated part of the overall site, blending in with the overall theme, which is defined in SCSS.
  3. Light mode and dark mode on mobile devices, and the ability to deal with the device auto-switching at dawn and dusk, are key things for our audience; dealing with that in SCSS using a media query is a reasonably simple way to handle this.

If that sounds like your needs, then this was a viable solution for us; it might work for you as well.

The solution is to use a class highlight style; admittedly took me a while to find this in the documentation, but it’s exactly what we needed. We wrap this with syntaxHighlighting() in the extensions.

import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
import {tags}                               from '@lezer/highlight';

// Use a class highlight style, so we can handle things in CSS.

const highlightStyle = HighlightStyle.define([
  { tag: tags.atom,      class: 'cmt-atom'      },
  { tag: tags.comment,   class: 'cmt-comment'   },
  { tag: tags.keyword,   class: 'cmt-keyword'   },
  { tag: tags.literal,   class: 'cmt-literal'   },
  { tag: tags.number,    class: 'cmt-number'    },
  { tag: tags.operator,  class: 'cmt-operator'  },
  { tag: tags.separator, class: 'cmt-separator' },
  { tag: tags.string,    class: 'cmt-string'    }
]);

We then define the following in our SCSS setup; cm-editor is obvious; cm-view is simply a class that we provide a pre element with when we’re dumping styled content into it via highlightTree(), much in the same way that one could use runMode() prior to version 6.

.cm-editor,
.cm-view {

  .cmt-atom      {color: #221199;}
  .cmt-comment   {color: #AA5500;}
  .cmt-keyword   {color: #8959A8;}
  .cmt-literal   {color: #4271AE;}
  .cmt-number    {color: #F5871F;}
  .cmt-operator  {color: #008803;}
  .cmt-separator {color: #990033;}
  .cmt-string    {color: #FF5500;}

  @include prefers-color-scheme($color-scheme-alt) {

    .cmt-atom      {color: #F78C6C;}
    .cmt-comment   {color: #545454;}
    .cmt-keyword   {color: #C792EA;}
    .cmt-literal   {color: #FFCB6B;}
    .cmt-number    {color: #FF5370;}
    .cmt-operator  {color: #89DDFF;}
    .cmt-separator {color: #FF7DE9;}
    .cmt-string    {color: #F07178;}
  }
}

This works well for our needs; code is automatically themed properly for the mode when the device switches modes.

2 Likes