Intro to Lit JS in 2026: Creating a dark mode
Posted onFor this blog I’ll be covering a StackBlitz demo I put together. You can find the demo here and below.
Once again there are two primary areas of concern when creating a dark mode in Lit.
- Architecting the CSS
- Creating a toggle Button
Architecting the CSS
The most critical part of creating a dark mode in Lit actually has nothing to do with Lit or Web Components. The most critical part is setting up your CSS properly. We do this with CSS Custom Properties. What you want to do is create a kind of color pallet. You then reference this pallet in all your Web Components. Since CSS Custom Properties pierce the Shadow DOM, this is easy.
You create a color pallet by defining your CSS Properties in a :root selector. You then override this with another selector that symbolizes how to activate dark mode. There’s two ways I prefer to do this. You can either:
- Use a media query that prefers dark mode
- Use an attribute like
[data-theme]to determine this.
Using the user’s preference with a media query
Here’s a media query that allows you to prefer dark mode:
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #121212;
--text-color: #f0f0f0;
--accent-color: #3793ff;
}
}
I’m mentioning this because a lot of the time you’ll want to do this instead of a manual toggle by the user on attribute. We want more control over when light/dark mode is toggled though. So this blog is gonna cover the manual toggle.
Using an attribute
HTML5 has a cool API called the Dataset API. It allows you to use any attribute prefixed with data-. If you do, you’ll have access to a .dataset property on that element. From there, any data you pass with the attribute is available as a property of dataset. This is perfect for our theming needs. It means that we can place a data-theme on our html tag to always know which theme we’re in, light or dark. Since the html tag is a root level tag, this serves as a type of “master scope”.
With that said, creating our pallet because a simple matter of overwriting our default CSS Custom Properties for the dark theme. Notice the following lines in my index.css:
:root {
--text: #16171d;
--background: #f8f8f8;
--border: #e5e4e7;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
...
}
[data-theme="dark"] {
/* for the dark theme, we swap the text and background colors */
--text: #f8f8f8;
--background: #16171d;
}
html {
color: var(--text);
background-color: var(--background);
}
The most important thing to pay attention to is the --text and –background properties. When selecting our dark theme we swap them. In essence, that is all you need to do. While advance theming gets more complicated than this, I’ll cover that topic in another blog. Since we’re using attributes to toggle the theme, we can write a toggle component for this. Before we do though…
Using a css file for your styles
.css file. This is not the standard. Since your css will create a constructed style sheet, the standard is to write css in your js. This is not like CSS in JS with JavaScript frameworks.
If you want to use a css file in your Lit Element, you can do so by importing the css file with ?inline appended to the path. So…
import styles from './my-app.css?inline'
This should work in a Rollup based build tool like Vite. It tells it to import the file contents as a string rather than an ESModule. Once you have the string, you can use unsafeCSS to transform that string into a CSSResults type that Lit needs. Like so:
static styles = [unsafeCSS(styles)];
Ignore the name “unsafeCSS”. It’s only named that because it’s warning you that you should always sanitize your css before using this. This assumes the css comes from an external source though. Our CSS is a file that we control so this is not necessary.
With that said, we’re now ready to take a look at how to toggle our data-theme attribute in a Lit Element.
Creating a Toggle Button
Our toggle button is simple. We have one action, toggle the theme. If you read my previous blog on using the DOM as a component model then you should know since we have one action we have one method. The method is going to be .toggleMode(). We’re going to call this when the button is clicked, so we also have one event.
In Lit we bind events by prefixing the event named with @. We can do this declaratively in our html. Also in Lit, we use the render() method to display UI. So our code to render a button with a click event looks like this:
render() {
return html`
<button @click=${() => this.toggleTheme()}>
<slot><slot>
</button>
`;
}
<slot> element is out of scope for this discussion. I’ll write about this later. For now just know our text from the button will appear here.
Actually toggling the theme is simple as well. With the dataset API we have a theme property in our dataset. Since light is the default theme, all we need to do is check for if the theme is set to dark. If it’s dark, toggle it to our default light theme. A cool thing about the Dataset API is that when we update it via JavaScript the html attribute “reflects” our changes. So when we toggle from light to dark, so does the attribute on the html. This means that we get our styles as defined before appropriately.
That’s almost it! But check out this line of code:
localStorage.setItem('demo-theme', newTheme);
It would be a shame if users had to manually click a button every time they wanted dark mode. With this line however, we leverage localStorage to remember their preference. That means that we need to read localStorage everytime the app loads. Checkout this in my-app.ts:
firstUpdated() {
const theme = localStorage.getItem('demo-theme');
if (theme) document.documentElement.dataset.theme = theme;
}
The method here is firstUpdated. This is a Lit Lifecycle Method. It fires when the element’s dom is ready. Inside it all we need to do is read localStorage for the theme. We then check for the theme and set the document element appropriately.
I hope you enjoyed walking through creating a dark mode with Lit. If you’re coming from a React background I hope you can appreciate the simplicity of working with standard APIs in JavaScript. No need for context or providers or any of that jazz. I’d love feedback on the beginner friendly-ness of this blog so feel free to reach out to me about it on social media. You can find all my socials on LinkTree.