# Dynamic light mode / dark mode - how?

**URL:** https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709
**Category:** v6
**Created:** [July 13, 2022, 4:12pm UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709 "2022-07-13T16:12:04Z")
**Posts on this page:** 8
**Page:** 1

<div class="post-metadata">

### Author: ![alb](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/alb/32/2266_2.png) [@alb](https://discuss.codemirror.net/u/alb)
#### Post date: [July 13, 2022, 4:12pm UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/1 "2022-07-13T16:12:04Z")

</div>

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

```auto
@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.

---

<div class="post-metadata">

### Author: ![eliitas](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/eliitas/32/2269_2.png) [@eliitas](https://discuss.codemirror.net/u/eliitas)
#### Post date: [July 13, 2022, 9:54pm UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/2 "2022-07-13T21:54:59Z")

</div>

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)](https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts)

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:

```auto
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
                    )
                });

```

---

<div class="post-metadata">

### Author: ![alb](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/alb/32/2266_2.png) [@alb](https://discuss.codemirror.net/u/alb)
#### Post date: [July 14, 2022, 1:52pm UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/3 "2022-07-14T13:52:54Z")

</div>

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:

> **[MediaQueryList: change event - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/change_event)**
>
> The change event of the MediaQueryList interface fires when the status of media query support changes.

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

```auto
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.

---

<div class="post-metadata">

### Author: ![alb](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/alb/32/2266_2.png) [@alb](https://discuss.codemirror.net/u/alb)
#### Post date: [July 14, 2022, 1:59pm UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/4 "2022-07-14T13:59:51Z")

</div>

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.

---

<div class="post-metadata">

### Author: ![alb](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/alb/32/2266_2.png) [@alb](https://discuss.codemirror.net/u/alb)
#### Post date: [July 18, 2022, 2:36pm UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/5 "2022-07-18T14:36:11Z")

</div>

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.

```auto
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.

```auto
.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.

---

<div class="post-metadata">

### Author: ![Neonnaut](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/neonnaut/32/4515_2.png) [@Neonnaut](https://discuss.codemirror.net/u/Neonnaut)
#### Post date: [July 13, 2024, 4:46am UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/6 "2024-07-13T04:46:44Z")

</div>

I was able to achieve this effect in typescript:

```auto
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, lineNumbers } from '@codemirror/view';

// Themes
import { xcodeLight, xcodeDark } from './themedist';
const themeConfig = new Compartment();

// Language
import { xlanguage } from "./langdist";

function createEditorState(initialContents, myTheme) {
    let extensions = [
        lineNumbers(),
        xlanguage(),
        themeConfig.of(themeIdentifier(myTheme))
    ];

    return EditorState.create({
        doc: initialContents,
        extensions
    });
}

function createEditorView(state, parent) {
    return new EditorView({ state, parent });
}

function themeIdentifier(myTheme) {
    switch (myTheme) {
        case 'light':
            return xcodeLight;
        case 'dark':
            return xcodeDark;
        default:
            return xcodeLight;
    }
}

function changeEditorTheme(myEditor, myTheme) {
    myEditor.dispatch({
        effects: themeConfig.reconfigure(themeIdentifier(myTheme))
    })
}

export { createEditorState, createEditorView, changeEditorTheme };

```

Then where I create the editor view:

```auto
<div id="editor"></div>
<button name="light">light</button>
<button name="dark">dark</button>
    
<script>
    // Now we can check for light/dark preference here before editor init..
    const initState = cm6.createEditorState("Example text", "light");
    const view = cm6.createEditorView(initState, document.getElementById("editor"));
        
    $(window).load(function () {
        $("[name='changetheme']").click(function () {
            cm6.changeEditorTheme(view, "dark");
        });
        $("[name='light']").click(function () {
            cm6.changeEditorTheme(view, "light");
        });
    });
</script>

```

---

<div class="post-metadata">

### Author: ![carueda](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/carueda/32/4716_2.png) [@carueda](https://discuss.codemirror.net/u/carueda)
#### Post date: [October 4, 2024, 4:06am UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/7 "2024-10-04T04:06:37Z")

</div>

Thanks! `Compartment` was the key element to solve issues I was having. This is in the context of a Vue component and using the Quasar framework. In case it’s useful to anyone stopping by, here’s the code:

```vue
<script setup lang="ts">
import { oneDark } from '@codemirror/theme-one-dark'
import { EditorView } from 'codemirror'
import { EditorState, Compartment } from '@codemirror/state'
import { basicSetup } from 'codemirror'
import { python } from '@codemirror/lang-python'
import { ref, watch, onMounted } from 'vue'
import { useQuasar } from 'quasar'

const codePython = `def foo():
  print('Hello, world!')
`

const editorTheme = new Compartment()

const q = useQuasar()
const editorView = ref<EditorView | null>(null)

const createEditorState = (isDarkMode: boolean) => {
  const theme = isDarkMode ? oneDark : []
  return EditorState.create({
    doc: codePython,
    extensions: [basicSetup, python(), editorTheme.of(theme)]
  })
}

const updateTheme = (isDarkMode: boolean) => {
  const theme = isDarkMode ? oneDark : []
  if (editorView.value) {
    // Dynamically reconfigure the theme extension properly
    editorView.value.dispatch({
      effects: editorTheme.reconfigure(theme)
    })
  }
}

onMounted(() => {
  const parent = document.getElementById('codemirror-editor')
  if (parent) {
    editorView.value = new EditorView({
      state: createEditorState(q.dark.isActive),
      parent: parent
    })
  }
})

watch(() => q.dark.isActive, (isDarkMode) => {
  updateTheme(isDarkMode)
})
</script>

<template>
  <q-page class="q-pa-lg">
    <div id="codemirror-editor"></div>
  </q-page>
</template>

<style>
#codemirror-editor {
  width: 100%;
  height: 20em;
}
</style>

```

---

<div class="post-metadata">

### Author: ![qbane](https://discuss.codemirror.net/user_avatar/discuss.codemirror.net/qbane/32/703_2.png) [@qbane](https://discuss.codemirror.net/u/qbane)
#### Post date: [September 18, 2025, 5:07pm UTC](https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/8 "2025-09-18T17:07:12Z")

</div>

I extracted minimal requirements mentioned in this thread and refactored your code into a reusable view plugin in Vanilla JavaScript (TypeScript actually). It is more adaptive to different frameworks:

```typescript
const SELECTOR_PREFERS_DARK_COLOR_SCHEME = '(prefers-color-scheme: dark)'

function autoColorScheme(options: {
  dark?: Extension, light?: Extension, defaultDark?: boolean} = {}): Extension {

  const {
    dark: darkExt = [],
    light: lightExt = [],
    defaultDark = false,
  } = options

  const darkThemeCompartment = new Compartment()

  function darkThemeExt(dark: boolean): Extension {
    return dark ? darkExt : lightExt
  }

  const plugin = ViewPlugin.define((view) => {
    function colorSchemeChangeHandler(evt: MediaQueryListEvent) {
      dispatchDarkTheme(evt.matches)
    }

    function dispatchDarkTheme(dark: boolean) {
      view.dispatch({
        effects: darkThemeCompartment.reconfigure(darkThemeExt(dark)),
      })
    }

    const win = view.dom.ownerDocument.defaultView
    const supportsMatchMedia = typeof win?.matchMedia === 'function'

    if (supportsMatchMedia) {
      const query = win.matchMedia(SELECTOR_PREFERS_DARK_COLOR_SCHEME)
      query.addEventListener('change', colorSchemeChangeHandler)
      if (query.matches !== defaultDark) {
        dispatchDarkTheme(query.matches)
      }
      return {
        destroy() {
          query.removeEventListener('change', colorSchemeChangeHandler)
        }
      }
    }

    // matchMedia is unsupported?
    return {}
  })

  return [
    plugin,
    darkThemeCompartment.of(darkThemeExt(defaultDark)),
  ]
}

```

You include an extension each for dark and light color scheme and the plugin switches between the two based on the media query. The `defaultDark` is a fallback value where `matchMedia` is not supported.
