Using @import in CSS to Conditionally Load Syntax Highlighting Styles in Dark Mode
NOTE: this post comes with a giant asterisk because styles wonât load conditionally. They just get applied conditionally. Read my update here.
Iâve been playing around with dark mode recently and found an interesting little trick I havenât seen published anywhere. Let me tell you about it.
You know how they always say âdonât use @import
in CSSâ? Well, thatâs probably true in a lot of cases, but I recently found a case where using it actually makes a lot of sense, and is quite elegant in comparison to other similar solutions.
The Problem
I recently incorporated dark mode into my blog, but it was a kind of âminimum viableâ dark mode. One of the places where I didnât fully polish my siteâs styles for dark mode was in the code syntax highlighting of my blog posts.
If you read my blog today (in âlight modeâ) youâll see colored syntax highlighting for the code examples. However, when you switch to dark mode you lose that âfeatureâ and all code just becomes white.
If you looked at the CSS for this, youâd see how I left myself a little @TODO comment saying âhey, you should get around to thisâ. I essentially had to overwrite all the âlight modeâ styles and say, âjust make everything white when in dark modeâ, as I hadnât had the time to figure out a dark color scheme and how to make it work (that *
selector is nested and ends up something like .markdown pre *
):
I finally got around to trying to make this a bit better and I found a nifty solution that uses @import
in CSS.
The Solution
My blog posts are rendered with markdown-it which gives you the ability to add syntax highlighting to your generated markup. The markup that gets generated has a bunch of classes that correspond to syntax highlighting themes from highlightjs.org. So getting âlight modeâ working was pretty straightforward: I added a <link>
to a highlight.js stylesheet hosted on a CDN and boom, I had syntax highlighting.
<link rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/default.min.css">
Now the question is: when the user is in âdark modeâ, how do I override the âlightâ styles Iâve <link>
âd to and instead leverage a different, themed stylesheet from highlight.js? At first I thought I was going to have to use JavaScript to detect (and listen for) when the user was in dark mode and then trigger something that swaps out stylesheets. Having worked with window.matchMedia
before, I figured that was the route I was going to go. But inside I was grumbling a bit, âthatâs a lot of code to write for something so simpleâ I thought.
Then I started thinking about @media (prefers-color-scheme: dark)
in CSS and how simple and declarative that was for handling light/dark mode. The declarative nature of CSS allows me to just say âwhen itâs dark, do this; when itâs light, do that.â Because I was already using <link>
in my HTML to include the necessary stylesheet, I thought to myself: wouldnât it be nice if I could just delaratively say use one or the other based on whether Iâm in dark or light mode? i.e. something like this:
<!-- If dark mode, use this -->
<link rel="stylesheet" href="cdn.com/atom-one-light.min.css">
<!-- Otherwise, assume its light mode and use this -->
<link rel="stylesheet" href="cdn.com/atom-one-dark.min.css">
I knew CSS already allowed you to do that logic using @media
queries, but it took me a second before I remembered the oft-neglected @import
at-rule, which does exactly what I was alluding to with the conditional <link>
tags in HTML. Then my brain made the jump: âwait a second, I could just pair @import
with @media
and Iâm done!â I was thinking something akin to:
/* By default, include the "light" color theme for syntax highlighting */
@import "cdn.com/atom-one-light.min.css";
/* And if youâre in dark mode, have those rules superseded via a different stylesheet */
@media (prefers-color-scheme: dark) {
@import "cdn.com/atom-one-dark.min.css";
}
I was close, but that actually doesnât actually work. The idea was there, I just had to figure out the right syntax. Mozillaâs great docs pointed out that â@import
cannot be used inside conditional group at-rules.â Instead, you actually declare conditional imports based on media queries (which I didnât know was a thing, and is actually pretty dang cool). So, in my case, I ended up with two lines of code:
/* Assume light mode by default */
@import "cdn.com/atom-one-light.min.css" screen;
/* Supersede dark mode when applicable */
@import "cdn.com/atom-one-dark.min.css" screen and (prefers-color-scheme: dark);
Thatâs pretty cool, and honestly feels like a perfect case for @import
. I am conditionally declaring styles for syntax highlighting based on whether the user is in dark mode or not. And I didnât have to use any JavaScript. No event listeners. No imperative fetch calls. Just two lines of CSS and it works.
It might not be apparent, but that gif showing the switch from light to dark is actually a whole different set CSS rules for doing syntax highlighting on that code.
@media (prefers-color-scheme: dark)
is still pretty new, which is why I donât use @media (prefers-color-scheme: light)
for the first @import
. I assume a light mode by default, but if the user has a newer browser and their system says theyâre in dark mode, theyâll get the dark styles. The idea of conditionally serving different styles to different devices based on information queried from @media
feels like the perfect use case for @import
in CSS.
Update: Apr 17, 2019
@tylergaw hit me up on twitter after this post, pointing out that you can use the media
attribute on the <link>
tag in HTML to essentially accomplish the same thing I was doing with @import
in CSS.
Yesterday's post was great. You're making me want to do light/dark styles for my site now. That import syntax is super cool, I'd never seen that. Could you also use the
media
attribute oflink
to accomplish the same thing?
Itâs funny because my questioning began at the HTML level, i.e. âwouldnât it be nice if, in HTML, I could declaratively say âuse this stylesheet if youâre in dark mode, otherwise use this oneââ. Turns out you can!
Doing it in HTML is even cooler because now I have control over when the <link>
appears in the HTML. For example, the CSS styles for syntax highlighting are really only applicable to âpostâ pages on my blog. For example, my âAboutâ page doesnât have any syntax highlighting on it, so including those styles is just dead weight. However, now that I can include these styles on a page-by-page basis, I can tap into my static site generator and only include the <link>
to dark mode syntax highlighting on applicable pages.
For example, I have a <Page>
component that wraps every single .html
page that gets output by my static site generator. Inside of that component, I can detect if the page itâs rendering is a âpostâ page. If it is, only then do I include a <link>
to the syntax highlighting styles. Example:
const Page = (props) =>
<html>
<head>
{/* Styles every page on my site needs */}
<link rel="stylesheet" href="assets/styles.css" />
{/* Styles only post pages need */}
{props.isPost &&
<>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/atom-one-light.min.css"
media="screen"
/>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/atom-one-dark.min.css"
media="screen and (prefers-color-scheme: dark)"
/>
</>}
</head>
</html>
Now only my post pages will have <link>
tags that retrieve syntax highlighting styles. And the browser takes care of fetching the right one, depending on whether the client is âin dark modeâ or not.
This is pretty damn cool. In the future, I can imagine a world where you can split up all your stylesheets by light/dark and then just tell the browser âfetch the one you need, based on user preferencesâ.
<link
rel="stylesheet"
href="light-mode-styles.css"
media="screen and (prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
href="dark-mode-styles.css"
media="screen and (prefers-color-scheme: dark)"
/>