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:
.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 (<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:
<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:
However, with very few lines of CSS, we can turn that into:
... and by adding just one CSS-declaration:
.dark {
color-scheme: dark;
}
— we get:
Styling the syntax highlight
First, we need some variables for the colors and basic CSS for the list itself:
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. These will automatically change in dark mode — more on that later.
Next, using native CSS nesting, we make sure that all the child-tags behave as we want:
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:
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:
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:
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
:
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
Top comments (2)
You inspired me!
How about this as a way to get rid of the
<span>
madness and let people just use a<pre>
element for code?About to drop an article on this, JavaScript syntax highlighting...using a single element, no
<span>
element and just CSS.And here is the generator to create the gradient background that you would actually serve up:
That’s pretty funky! 🤓