Syntax-Highlight CSS with Semantic HTML — and get Dark Mode for free
Tags: css, html, webdev, showdev
Published: 2023-08-12 15:16:54

When we syntax highlight CSS-code, we typically use a JavaScript library, that turns the CSS into a bunch of `<span>`-tags with different `class`’es.

I thought it could be fun to `HTML`’ify the CSS and turn it into meaningful semantics. But which tags should represent a CSS rule? 

CSS is written as **rules**, which consist of a **selector group** and a **declaration block**.

The latter contains one or more **property/value**-pairs.


_Example:_
```css
.selector {
  /* declaration block */
  property: value;
}
```

A stylesheet — at least to me — is like an unordered list (`<ul>`), with each item (`<li>`) representing a **rule**. 

Any heading tag (`<h2>, <h3>` etc.) will work as the **selector group** — and a [description list](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl) (`<dl>`) is the perfect candidate for the **declaration block**.

A CSS property will be a _term_, using the `<dt>`-tag, and each CSS value will be a _description_, using the `<dd>`-tag.

The `<var>`-tag will be used for _CSS Custom Properties._

_Example:_
```html
<ul>
  <li>
    <h3>body</h3>
    <dl>
      <dt>accent-color</dt>
      <dd><var>AccentColor</var></dd>
      <dt>background-color</dt>
      <dd>Canvas</dd>
      <dt>color</dt>
      <dd><var>ColorGray-80</var></dd>
      <dt>color-scheme</dt>
      <dd>light dark</dd>
      <dt>font-family</dt>
      <dd><var>ff-sans</var></dd>
      <dt>font-size</dt>
      <dd>clamp(1rem, 0.8661rem + 0.4286vw, 1.1875rem)</dd>
      <dt>line-height</dt>
      <dd>1.5</dd>
      <dt>margin-inline</dt>
      <dd>auto</dd>
      <dt>max-inline-size</dt>
      <dd>70ch</dd>
      <dt>padding-inline</dt>
      <dd>2ch</dd>
    </dl>
  </li>
  <li>
    <h3>img</h3>
    <dl>
      <dt>height</dt>
      <dd>auto</dd>
      <dt>max-width</dt>
      <dd>100%</dd>
  </li>
  <li>
    <h3>td</h3>
    <dl>
      <dt>border</dt>
      <dd>1px solid</dd>
      <dt>font-size</dt>
      <dd>smaller</dd>
      <dt>padding</dt>
      <dd>1ch</dd>
  </li>
</ul>
```

Out-of-the-box this doesn't look particularly great:

![Syntax Highlight without styles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gipoyr4a2au08qk9cl7o.png)

However, with very few lines of CSS, we can turn that into:

![Syntax Highlight Light Mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bad4jsx34h4bfkagxkr3.png)

... and by adding just **one** CSS-declaration:

```css
.dark {
  color-scheme: dark;
}
```

— we get:

![Syntax Highlight Dark Mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lmngny973hz6n4elwno5.png)

---

## Styling the syntax highlight

First, we need some variables for the colors and basic CSS for the list itself:

```css
ul {
  --_property: color-mix(in srgb, HighLight, CanvasText 30%); 
  --_selector: cornflowerblue;
  --_value: orange;

  background: color-mix(in srgb, Canvas 95%, CanvasText 5%);
  font-family: ui-monospace,  monospace;
  list-style: none;
  margin: 0;
  overflow: auto;
  padding: 2ch;
}
```

The `color-mix`-method is used here with [system colors](https://developer.mozilla.org/en-US/docs/Web/CSS/system-color). These will automatically change in [dark mode](https://dev.to/madsstoumann/dark-mode-in-3-lines-of-css-and-other-adventures-1ljj) — more on that later.

Next, using native CSS nesting, we make sure that all the child-tags behave as we want:

```css
ul {
  & * {
    font-size: 1em;
    font-style: normal;
    font-weight: 400;
    margin: 0;
    white-space: nowrap;
  }
}  
```

For the _selector_, we allow `<em>`, `<strong>` or any `<h>`eading-tag. We add a `{`-char after, and after each _rule_ (`<li>`), we add `}` and a line-break:

```css
ul {
  & :is(em, h2, h3, h4, h5, h6, strong) {
    color: var(--_selector);
    &::after { content: " {"; }
  }
  & li::after {
    color: var(--_selector);
    content: "}\a"; white-space: pre;
  }
}
```

For the `<var>`iables, we add:

```css
ul {
  & var {
    &::before { content: "var(--"; }
    &::after { content: ")"; }
  }
}
```

We want to display the property/value-pairs as `inline`, so they don't break to separate lines. We add the `:`-char after each _property_ — and for the _value_, we want to end the line with a `;`-char and a line-break:

```css
ul {
  & dd, & dt { display: inline; }
  & dd {
    color: var(--_value);
    &::after {
      content: ";\a";
      white-space: pre;
    }
  }
  & dl { margin: 0 0 0 2ch; }
  & dt {
    color: var(--_property);
    &::after { content: ":"; }
  }
}
```

And that's it! 

---

## Dark Mode for free

Because we used _system colors_, we get dark mode for free. 

Add `color-scheme: light dark;` to your `body`-styles, and change your OS to either light or dark mode, and you should see the syntax-styles update.

The great thing about `color-scheme`, is that you can **force** it to one or the other, even if your OS is set to the opposite. 

If your OS is set to "light mode" and you set `color-scheme: dark` on the `<ul>`-tag, it'll render as "dark mode", even if the rest of the page is in "light mode".

---

## Code
For your reference, here's the complete chunk of CSS — give it a meaningful `class`-name instead of `ul`:

```css
ul {
  --_property: color-mix(in srgb, HighLight, CanvasText 30%); 
  --_selector: cornflowerblue;
  --_value: orange;

  background: color-mix(in srgb, Canvas 95%, CanvasText 5%);
  font-family: ui-monospace, monospace;
  list-style: none;
  margin: 0;
  overflow: auto;
  padding: 2ch;
  & * {
    font-size: 1em;
    font-style: normal;
    font-weight: 400;
    margin: 0;
    white-space: nowrap;
  }
  & dd, & dt { display: inline; }
  & dd {
    color: var(--_value);
    &::after {
      content: ";\a";
      white-space: pre;
    }
  }
  & dl { margin: 0 0 0 2ch; }
  & dt {
    color: var(--_property);
    &::after { content: ":"; }
  }
  & :is(em, h2, h3, h4, h5, h6, strong) {
    color: var(--_selector);
    &::after { content: " {"; }
  }
  & li::after {
    color: var(--_selector);
    content: "}\a"; white-space: pre;
  }
  & var {
    &::before { content: "var(--"; }
    &::after { content: ")"; }
  }
}
```
---

> **NOTE:** The code use native nesting and `color-mix`, which are currently _behind-a-flag_ or in "nightly"-versions of browsers.

---

Cover Image by DALL·E, using the prompt:

> Using the terms "cascading style sheets" and "syntax highlight", generate a painting in the style of Salvador Dali