Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 2,016 words · 6 segments analyzed
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.12. We can Describe Sets of ThingsWe 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 ThingsIt’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 ThatBut 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 ExampleHere’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". (“But what if that’s bad design?” The PM insists that it is, and the manager likes her more than you, so.)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 (well, sort of, see the end).4. But What If You CouldImagine a version of CSS which we will call CSSLog, for Reasons which you may guess but will eventually become clearer.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? Destroy elements?!? (Probably not.) Something like:div.foo { class: +bar /* Add the class bar */ +<div class="baz"> /* Add a child element */ }
div.bar { /* The element which previously had .foo, once the above rule runs, now also has .bar, and also matches here! */ } What’s the worst that could happen?5. Are You Out Of Your God Damn MindProbably. But look, there’s more precedent for this than you might expect. It just is usually written differently.In a very different world from the crisp Retina screens of CSS designers, buried in dingy university labs and esoteric former conferences and maybe occasionally an internal department at a big tech company if you’re lucky, 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). The hell is that? They’re calling it “Datalog”. (Also, that’s where I got the name “CSSLog” from.)What are those?
Function calls? What’s :- mean? What’s with all the periods, are they object-oriented property access … oh god, are they trying to do that thing like SQL from the 70s where they try to use punctuation and stuff to look like English sentences? Where did alice, bob, X, and Y even come from, anyway? I don’t recall seeing anyone declare them with a var or let or anything sensible like that. And what does this even have to do with CSS?It’s surprisingly similar. Let’s go through the steps:5.1. There are ThingsIn this case, the Things are called atoms. Atoms spring into existence when they’re first mentioned, there is no “declaration before use”. alice and bob are atoms. (If you’re familiar with e.g. Ruby, you can compare them to :symbols.)5.2. We can Describe Sets of ThingsIn Datalog, we do this with relations. A relation is a set of tuples (this is also the definition of a SQL Table, not entirely coincidentally). A tuple is a list of atoms. E.g. in the example above, parent is a relation. parent(alice, bob) is a tuple in the parent relation. The parent relation is a set of pairs, such as the (alice, bob) pair, indicating “Thing 1 in this pair is the parent of Thing 2”.We can select things that match a query with variables. The following:parent(bob, X) is read as “All X such that (bob, X) is a tuple in the parent relation”, or, “All X such that Bob is the parent of X”. In this case, X would evaluate to a set of atoms, those being carol and dave. X is a variable. (Conventionally, variables are upper case and atoms or relations are lower case.)We can also intersect sets, just like CSS can. This is usually called a join. Repeating the same variable name twice in a rule body joins on that variable:% These are unary relations, aka sets of atoms. Also yeah comments use `%`.
woman(alice). man(bob). parent(alice, bob). parent(bob, carol).
% "X is the mother of Y, if X is the parent of Y, and X is a woman."
% X was repeated in the body, so it's a join. mother(X, Y) :- parent(X, Y), woman(X). The example above essentially intersects “the set of all parents” with “the set of all women”, to form “the set of all mothers”.A Datalog rule looks like this:head(X, Y) :- body1(X, Z), body2(Z, Y). Read :- as “if”. The right side is your body — a list of conditions, all of which must hold simultaneously. The left side is your head — the new fact you’re asserting is true whenever the body holds. Commas in the body are “and”.So ancestor(X, Y) :- parent(X, Y). means: “For all possible values of X and Y, X is an ancestor of Y, if X is a parent of Y.”To make the comparison explicit:% "If X is a div, and X has class awesome, then X has color red."
color(X, red) :- div(X), class(X, awesome). /* "If X is a div, and X has class awesome, then X has color red." Except we don't write the X. */ div.awesome { color: red; } Datalog and CSS look like each other, but backwards. The selector is the body. The declarations are the head. :- is { (err, sort of). We’ve been writing logic rules this whole time!5.3. We can Do Stuff to Those ThingsIn Datalog, “doing stuff” doesn’t just mean “setting a color”. It means deriving new facts — asserting that new tuples belong to relations, based on existing ones.Let’s look at the “ancestors” example again, which is the one that shows up in every Datalog text ever, and who am I to break the tradition:parent(alice, bob). parent(bob, carol). parent(bob, dave).
ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y). The first rule says: parents are ancestors. Simple. The second rule says: if X is a parent of Z, and Z is already known to be an ancestor of Y, then X is also an ancestor of Y. Notice that ancestor appears in both the head and the body of the second rule.
It refers to itself. It’s recursive. Run this on the facts above and you get:ancestor(alice, bob). % direct, from rule 1 ancestor(bob, carol). % direct ancestor(bob, dave). % direct ancestor(alice, carol). % alice -> bob -> carol, from rule 2 ancestor(alice, dave). % alice -> bob -> dave, from rule 2 This is something SQL couldn’t do before the WITH RECURSIVE keyword, which exists precisely because people kept needing to do stuff like this. (In typical SQL fashion, WITH RECURSIVE lets you express any recursive computation, but only if you shoehorn it into a weird syntax and semantics that doesn’t always compose well with other parts of the language.). It’s something CSS definitely can’t do. But it’s literally the first textbook example for Datalog.Notice how I never wrote a for loop. We didn’t have to explicitly say “keep going until you’ve got everything”. The Datalog engine just… figures it out. How?6. The Fix is FixpointsIn normal CSS, the “cascade” is one forward pass: the browser reads all the rules, figures out which selectors match, and applies declarations. There’s no feedback loop.In CSSLog (and in actual Datalog), a rule can set an attribute that causes another rule to fire that sets another attribute that causes the first rule to fire again. So you can’t just do one pass. You have to keep going. But where do you stop?Here is how a naïve Datalog engine works (informally): Start with your base facts — the ones you wrote down explicitly, like parent(alice, bob). Look at every rule. Match the “body” against the currently known facts, substituting in values for variables in the process. For each such match, add the “head” of the rule to your list of known facts. If you added anything new in step 3, go back to step 2. If you didn’t, stop. You’re done. This is called “naive evaluation”. It runs until the set of known facts stops growing, which is called the fixpoint — the point where applying all the rules produces nothing you didn’t already have.