CSS as a Query Language

An investigation into using CSS as a query language or general-purpose programming language, exploring its fundamental principles and its surprising connection to Datalog.
IN which we investigate using CSS as a query language, or even a general purpose programming language, to do things other than lay out web pages in a browser.
Question: Why in God’s good name would you do that? CSS is infamously confusing. And better query languages exist, right? Such as SQL, which famously doesn’t have problems.
Answer: Because it’s there.
The basic principles of CSS look like this.
1. There are Things
“Things” are “domain entities”, or “atoms”, or “facts”. They exist outside of CSS – from CSS’s perspective, they’re just already and always there.
Such as:
<h1>Hello, World!</h1>
<a href="example.com">This is a link</a>
<div class="awesome" data-custom-attribute="foo">
<div id="child">This div is inside another one!</div>
</div>
Specifically, here, Things are HTML elements.
2. We can Describe Sets of Things
We can write down selectors which refer to sets of Things that all have something in common.
/* The set of all things that are a div */
div
/* The set of all things with the id "child", which is just one thing */
#child
/* The set of all things with the class "awesome" */
.awesome
/* The set of all things having an attribute `data-custom-attribute` with value "foo" */
[data-custom-attribute="foo"]
We can also describe things based on their position in the document hierarchy relative to each other, a handy feature when our “Things” are HTML Elements, which tend to go inside each other.
We can also combine selectors to perform set intersection on the Things they describe. This turns out to be crucial:
div.awesome /* The set of all things that are divs, AND have the class "awesome" */
3. We can Do Stuff to Those Things
It’s not a very useful language to just describe sets of things in isolation. In CSS, we define rules that pair a selector (which selects a set of elements) with declarations, which describe what we would like to do with the elements in that set.
div.awesome {
color: red;
font-size: 24px
}
This says: “For all elements which are divs and have class ‘awesome’, set these properties (color and font-size) to these values”. This has the effect, in your web browser, of making these parts of the HTML page have giant red text. Pretty cool, right? Now you can be a 90s web designer.
3a. Except Not That
But this has some pretty big limitations. For the most part, these declarations change properties of elements that are – like elements themselves – outside the language. In other words: you can set an element’s color, but you can’t select on an element’s color:
/* Your browser will reject this: */
div[color=red] {
color: blue; /* What would this even mean? */
}
This would, admittedly, be kind of confusing. What does it mean to say “for all elements with color red, their color is blue”? Does it render red for a second and then flicker to blue? Does it flicker back and forth? Does it just say this is a contradiction, like 3 = 4, and give up?
There’s a way to answer this. We’ll get there.
3b. An Actual Example
Here’s something you might (possibly) actually want to do as a web developer.
You’re building a design system. You have a “dark mode” aware component — a card with data-theme="dark" — and you want every interactive element anywhere inside it, no matter how deeply nested, to get inverted focus styles. Not just direct children, but any descendant, transitively, unless some intermediate component has explicitly opted out with data-theme="light".
In real CSS, you write:
[data-theme="dark"] :focus {
outline-color: white;
}
/* Undo it if there's a light-theme ancestor in between */
[data-theme="dark"] [data-theme="light"] :focus {
outline-color: black;
}
This works … for one level of nesting. Now what if there’s a dark card inside a light panel inside a dark page? You add another rule. And another. You are now writing an ad hoc, informally specified version of a transitive query.
What you actually want to say is: “an element is effectively-dark if it has data-theme=”dark”, or if it has an effectively-dark ancestor with no effectively-light ancestor in between.” That’s a recursive relational definition. CSS cannot express it. CSSLog can:
[data-theme="dark"] {
class: +effectively-dark; /* Adds the class with our hypothetical syntax. */
}
.effectively-dark > :not([data-theme="light"]) {
class: +effectively-dark;
}
.effectively-dark :focus {
outline-color: white;
}
The second rule propagates effectively-dark down through children, unless it hits an explicit light boundary. It runs recursively, until it’s satisfied with itself that some sort of desired goal state has been reached, and then stops. CSS cannot do this today.
4. But What If You Could
Imagine a version of CSS which we will call CSSLog.
In CSSLog, just like regular CSS, you can write selectors, that match elements, and set properties on those elements. BUT, those selectors can:
- Set properties of elements which affect whether other selectors match them, like
class. - Create new elements?
5. Are You Out Of Your God Damn Mind
Probably. But look, there’s more precedent for this than you might expect. It just is usually written differently.
In a very different world, people are writing code that might look something like this:
parent(alice, bob).
parent(bob, carol).
parent(bob, dave).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
They’re calling it “Datalog”. (Also, that’s where I got the name “CSSLog” from.)
It’s surprisingly similar. Let’s go through the steps:
5.1. There are Things
In this case, the Things are called atoms. Atoms spring into existence when they’re first mentioned. alice and bob are atoms.
5.2. We can Describe Sets of Things
In Datalog, we do this with relations. A relation is a set of tuples. A tuple is a list of atoms. We can select things that match a query with variables like X and Y.
Source: Hacker News
















