Agent skill
dynamic-themes-with-codemirror
Learn how to create a Lit web component with CodeMirror, dynamically themed using Material Design's color utilities, for a customizable code editing experience.
Install this agent skill to your Project
npx add-skill https://github.com/rodydavis/skills/tree/main/skills/codemirror-dynamic-theme
Metadata
Additional technical details for this skill
- last modified
- Tue, 03 Feb 2026 20:04:24 GMT
SKILL.md
Dynamic Themes with CodeMirror
In this article I will go over how to set up a Lit web component and use it to create a a code window that uses CodeMirror and apply a dynamic theme with Material Design.
Prerequisites
- Vscode
- Node >= 16
- Typescript
Getting Started
We can start off by navigating in terminal to the location of the project and run the following:
npm init @vitejs/app --template lit-ts
Then enter a project name codemirror-dynamic-theme and now open the project in vscode and install the dependencies:
cd codemirror-dynamic-theme
npm i lit codemirror @material/material-color-utilities
npm i -D @types/node @types/codemirror
code .
Update the vite.config.ts with the following:
import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
base: "/codemirror-dynamic-theme/",
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
},
},
},
});
Template
Open up the index.html and update it with the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeMirror Dynamic Theme</title>
<script type="module" src="/src/code-window.ts"></script>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<code-window> </code-window>
</body>
</html>
Web Component
Before we update our component we need to rename my-element.ts to code-window.ts
Open up code-window.ts and update it with the following:
import { html, css, LitElement, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
applyTheme,
argbFromHex,
hexFromArgb,
themeFromSourceColor,
} from "@material/material-color-utilities";
import CodeMirror from "codemirror";
import codemirrorStyles from "codemirror/lib/codemirror.css";
@customElement("code-window")
export class CodeWindow extends LitElement {
static styles = css`
${unsafeCSS(codemirrorStyles)}
main {
width: 100vw;
height: 100vh;
background-color: var(--md-sys-color-background);
color: var(--md-sys-color-on-background);
--header-height: 48px;
--input-size: 32px;
}
.toolbar {
height: var(--header-height);
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
display: flex;
align-items: center;
}
.actions > * {
margin-left: 4px;
margin-right: 4px;
}
.toolbar .title {
font-family: sans-serif;
font-size: 18px;
padding-left: 4px;
}
.toolbar .actions {
display: flex;
align-items: center;
}
.toolbar a {
padding: 0;
margin: 0;
padding-left: 8px;
padding-right: 8px;
display: flex;
align-items: center;
cursor: pointer;
}
input[type="color"] {
width: calc(var(--input-size) * 2);
height: var(--input-size);
outline: none;
border: none;
border-radius: 50%;
background-color: var(--md-sys-color-primary-container);
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: var(--input-size);
border: var(--md-sys-color-outline) solid 1px;
}
button {
border: none;
border-radius: 4px;
padding: 8px;
}
.tertiary {
background-color: var(--md-sys-color-tertiary);
color: var(--md-sys-color-on-tertiary);
}
.secondary {
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
}
.spacer {
flex: 1;
}
.editor {
height: calc(100% - var(--header-height));
width: 100%;
}
`;
@property() value = [
`import {html, css, LitElement} from 'lit';`,
`import {customElement, property} from 'lit/decorators.js';`,
``,
`@customElement('simple-greeting')`,
`export class SimpleGreeting extends LitElement {`,
` static styles = css\`p { color: blue }\`;`,
``,
` @property()`,
` name = 'Somebody';`,
``,
` render() {`,
` return html\`<p>Hello, \${this.name}!</p>\`;`,
` }`,
`}`,
].join("\n");
@property() color = "#6750A4";
@property({ type: Boolean }) dark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
render() {
return html`<main>
<header class="toolbar">
<div class="title">${document.title}</div>
<div class="spacer"></div>
<div class="actions">
<button class="secondary" @click=${this.toggleDark.bind(this)}>
${this.dark ? "Light" : "Dark"}
</button>
<button class="tertiary" @click=${this.randomColor.bind(this)}>
Random
</button>
<input
type="color"
.value=${this.color}
@input=${this.onColor.bind(this)}
/>
</div>
</header>
<div class="editor"></div>
</main>`;
}
firstUpdated() {
const root = this.shadowRoot!.querySelector(".editor") as HTMLElement;
const editor = CodeMirror(root, {
value: this.value,
mode: "javascript",
lineNumbers: true,
lineWrapping: true,
indentUnit: 4,
tabSize: 4,
indentWithTabs: true,
autofocus: true,
});
console.debug(editor);
editor.setSize("100%", `100%`);
this.updateTheme();
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
this.dark = e.matches;
this.updateTheme();
});
}
private updateTheme() {
// TODO: Generate Theme
}
private setColor(val: string) {
this.color = val;
this.updateTheme();
}
private onColor(e: Event) {
const target = e.target as HTMLInputElement;
this.setColor(target.value);
}
private randomColor() {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
this.setColor(color);
}
private toggleDark() {
this.dark = !this.dark;
this.updateTheme();
}
}
declare global {
interface HTMLElementTagNameMap {
"code-window": CodeWindow;
}
}
Here we are setting up some of the editor basics to load in the styles needed for the basic layout.
There are also few methods that handle updating of properties on the element such as toggleDark and setColor. When you run the application you should see the following:
It doesn't look great yet, but now we can add a CodeMirror theme to import. Create a file src/theme.ts and update it with the following:
import { css } from "lit";
export const codeMirrorTheme = css`
.CodeMirror {
background-color: var(--md-sys-color-background);
color: var(--md-sys-color-on-background);
}
.CodeMirror-gutters {
background: var(--md-sys-color-surface-variant);
color: var(--md-sys-color-on-surface-variant);
border: none;
}
.CodeMirror-guttermarker,
.CodeMirror-guttermarker-subtle,
.CodeMirror-linenumber {
color: var(--md-sys-color-on-background);
}
.CodeMirror-cursor {
border-left: 1px solid var(--md-sys-color-primary);
}
.cm-fat-cursor .CodeMirror-cursor {
background-color: var(--md-sys-color-background);
}
.cm-animate-fat-cursor {
background-color: var(--md-sys-color-background);
}
div.CodeMirror-selected {
background: var(--md-sys-color-surface-variant);
}
.CodeMirror-focused div.CodeMirror-selected {
background: var(--md-sys-color-surface-variant);
}
.CodeMirror-line::selection,
.CodeMirror-line > span::selection,
.CodeMirror-line > span > span::selection {
background: var(--md-sys-color-surface-variant);
}
.CodeMirror-line::-moz-selection,
.CodeMirror-line > span::-moz-selection,
.CodeMirror-line > span > span::-moz-selection {
background: var(--md-sys-color-surface-variant);
}
.CodeMirror-activeline-background {
background: var(--md-sys-color-surface);
}
.cm-keyword {
color: var(--md-custom-color-keyword) !important;
}
.cm-operator {
color: var(--md-custom-color-operator) !important;
}
.cm-variable-2 {
color: var(--md-custom-color-variable-2) !important;
}
.cm-variable-3,
.cm-type {
color: var(--md-custom-color-variable-3) !important;
}
.cm-builtin {
color: var(--md-custom-color-builtin) !important;
}
.cm-atom {
color: var(--md-custom-color-atom) !important;
}
.cm-number {
color: var(--md-custom-color-number) !important;
}
.cm-def {
color: var(--md-custom-color-def) !important;
}
.cm-string {
color: var(--md-custom-color-string) !important;
}
.cm-string-2 {
color: var(--md-custom-color-string-2) !important;
}
.cm-comment {
color: var(--md-custom-color-comment) !important;
}
.cm-variable {
color: var(--md-custom-color-variable) !important;
}
.cm-tag {
color: var(--md-custom-color-tag) !important;
}
.cm-meta {
color: var(--md-custom-color-meta) !important;
}
.cm-attribute {
color: var(--md-custom-color-attribute) !important;
}
.cm-property {
color: var(--md-custom-color-property) !important;
}
.cm-qualifier {
color: var(--md-custom-color-qualifier) !important;
}
.cm-variable-3,
.cm-type {
color: var(--md-custom-color-variable-3) !important;
}
.cm-error {
color: var(--md-sys-color-on-error);
background-color: var(--md-sys-color-error);
}
.CodeMirror-matchingbracket {
text-decoration: underline;
color: var(--md-sys-color-on-surface);
}
`;
Here we are defining all the styles as CSS Custom Properties so we can easily update them.
Now import the theme and apply the styles in the component:
import { codeMirrorTheme } from "./theme";
@customElement("code-window")
export class CodeWindow extends LitElement {
static styles = css`
${unsafeCSS(codemirrorStyles)}
${codeMirrorTheme}
...
Now we need to implement the updateTheme method in our element:
updateTheme() {
const source = this.color;
const dark = this.dark;
const target = this.shadowRoot!.querySelector("main") as HTMLElement;
const properties = [
`--md-custom-color-keyword: #c75779;`,
`--md-custom-color-operator: #008800;`,
`--md-custom-color-variable: #90ccff;`,
`--md-custom-color-variable-2: #dd7700;`,
`--md-custom-color-variable-3: #3333bb;`,
`--md-custom-color-variable-3: #decb6b;`,
`--md-custom-color-builtin: #003388;`,
`--md-custom-color-atom: #bb4646;`,
`--md-custom-color-number: #b4c5ff;`,
`--md-custom-color-def: #82aaff;`,
`--md-custom-color-string: #ffb4a9;`,
`--md-custom-color-string-2: #ffb4a9;`,
`--md-custom-color-comment: #888888;`,
`--md-custom-color-tag: #000080;`,
`--md-custom-color-meta: #a9c7ff;`,
`--md-custom-color-attribute: #008080;`,
`--md-custom-color-property: #336699;`,
`--md-custom-color-qualifier: #690;`,
];
const customColors = properties.map((property) => {
const [key, value] = property.split(":");
const name = key.trim().replace(/^--md-custom-color-/, "");
const color = argbFromHex(value.trim().replace(";", ""));
return {
name,
value: color,
blend: true,
};
});
const theme = themeFromSourceColor(argbFromHex(source), customColors);
applyTheme(theme, { target, dark });
for (const custom of theme.customColors) {
const name = custom.color.name;
const section = dark ? custom.dark : custom.light;
target.style.setProperty(
`--md-custom-color-${name}`,
hexFromArgb(section.color)
);
}
}
Here we are using the Material Color Utilities package and generating a theme from a source color. We can take advantage of Custom Colors to blend the values to the theme.
After the theme is generated we can apply them to the root element and have the custom properties update the editor.
Changing the source color can update the theme:
Changing the brightness can set the colors as well:
Conclusion
If you want to learn more about building with Lit you can read the docs here.
The source for this example can be found here.
If you want to check out dynamic themes for VSCode I created an extension here.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
how-to-do-offline-recommendations-with-sqlite-and-gemini
Learn how to enhance your CMS like PocketBase with AI-powered content recommendations using text embeddings, SQLite, and k-nearest neighbor search for efficient and scalable related content suggestions.
how-to-build-a-webrtc-signal-server-with-pocketbase
Learn how to build a simple WebRTC video call application using PocketBase as a signaling server, enabling peer-to-peer communication with SQLite on the server and realtime updates via Server Sent Events.
flutter-terminal-cheat-sheet
This post provides a handy collection of Flutter commands and scripts for web development, package creation, troubleshooting, testing, and more, streamlining your Flutter workflow.
how-to-print-multiple-objects-to-the-console-with-print-in-dart
Learn how to print multiple objects to the console in Dart using Records, offering a similar experience to JavaScript's `console.log()` functionality.
flutter-input-output-preview
Build responsive Flutter apps with a reusable `TwoPane` widget and an `InputOutputPreview` component for side-by-side code and preview display on both mobile and desktop.
displaying-html-in-flutter
Easily display and interact with HTML content in your Flutter app using the `easy_web_view` package, which supports both web and mobile platforms.
Didn't find tool you were looking for?