Pavel Panchekha

By

Share under CC-BY-SA.

Should CSS be Constraints?

CSS is hard. The layout rules are quite complex and hard to pick up just from examples. Something like "centering a <div>" is, like, famously a problem. Or remember the 2000s when you'd read A List Apart for all sorts of crazy ways to achieve the "Holy Grail" layout, specifically a header, a main body and sidebar of equal heights, and a footer? Given that it's such a mess, should we maybe just throw it out and start over with a totally different system?

I do feel like I can speak on this some authority. In grad school I wrote the first formal specification of the CSS 2.2 spec; the formal spec passed (the relevant fragment of) the conformance test. So I know the existing algorithm in quite a lot of detail, though of course I'm going beyond that expertise when I talk about what designers do or about other systems.

Constraints

One commonly-proposed replacement for CSS is a constraint system. In a constraint system you can just directly say:

(obj.top + obj.bottom) / 2 ==  (obj.parent.top + obj.parent.bottom) / 2

This line constrains the vertical midpoint of obj to be the vertical midpoint of its parent; in other words, it constrains it to be vertically centered.

And in fact this idea has been explored quite a bit. In CSS, there's the well-known Constraint cascading style sheets paper. The idea in that paper is that the web page author writes constraints, somewhat like the one above, and then the browser runs a constraint solver which computes positions and sizes for each object that satisfy the constraints. This is much like normal CSS, where the web page author writes rules and then the browser runs a layout algorithm that computes sizes and positions.1 [1 Naturally in both cases you actually compute a lot more than just sizes and positions: fonts, colors, and so on. But layout is taken to be the "hard part" of the problem, and I don't really disagree with that.]

In fact the authors (especially Alan Borning) have a long history with constraint solvers, and in particular are associated with the Cassowary incremental constraint solver, where "incremental" means it can not only solve the constraints quickly but also re-solve them extra-quickly if the page changes a small amount, like in response to JavaScript or user action. Real browsers do that too. And they've been quite successful. Most notably, iOS provides constraint-based using a re-implementation of Cassowary, and I hear it's quite popular.

What's wrong with constraints

All that said, I don't think a constraint system would actually be better for the web. With rule-based systems like current CSS, the challenge is that the rules are really complex and it's hard to predict what the layout will be because actually executing the rules in your head is nearly impossible. But with constraint-based systems, the layout might be literally under- or over-determined, in the sense that there might be more than one, or less than one, layouts that satisfy your rules.

In fact, writing layout constraints for UIs that aren't either under- or over-determined is basically impossible and I'm not sure anyone has ever done it. Even the simple "vertical centering" constraint above doesn't fix the size or position of either box. It doesn't say that the outer box (obj.parent) should be the minimum possible size to contain its child (obj), or that they should both be onscreen, or whatever. Maybe you'd write other constraints to achieve that, but the more constraints you write, the greater the chance those constraints will conflict, leaving you over-determined.

There's a bunch of things you could do at this point. For under-determined layouts, you could add "implicit rules", like saying that boxes should always be onscreen. Or "optimization criteria", like saying that if multiple layouts are available you should pick the one that takes up the least space. And for over-determined layouts, you could do the reverse, like assigning "weights" to each constraint and then optimizing for breaking the fewest. This isn't a crazy way to build a system—it's basically how LaTeX works—but fiddling with the weights, the exact form of implicit rules, the optimization criteria becomes, in practice, the actual determiner of how layouts look. And it turns out that simple implicit rules / criteria / weights lead to bizarre, horrible edge cases, so if you want things to look good you'll need really complicated ones, and then you're back to the actual layout being hard to predict from the style sheet.

In fact, LaTeX is not widely beloved for its predictable layout. And while constraint layout is, I believe, popular on iOS, it's notable that iOS quite famously allows only a small number of screen / window shapes. And also people still complain about the constraint-based layout being fussy, brittle, and unpredictable, with debugging (especially debugging under- and over-constrained layouts) considered tedious and annoying. Plus, people say it's verbose; given that there actually are conventions and patterns to how people design UIs, it would be nice to actually express those patterns and be modular with regard to them, and constraint-based systems, especially once you start adding implicit rules or optimization criteria, are quite explicitly not modular.

I do applaud Apple for shipping and I'm happy people are using it and it solves their problems. LaTeX solves mine!2 [2 Newer students seem really excited about Typst, which I have not tried but.] But I do think designers in constraint-based systems suffer exactly the kinds of problems theory would predict.

What's the underlying problem?

So why is this so hard? Why do we get all these weird edge cases pop up whenever we do layout? I think the issue is that layout actually is quite hard.

Here, let me give you an example. Here's a line from my formal semantics of CSS 2.2, the specification of text-align: center:

[(is-text-align/center textalign)
 (= (- (left-outer f) (left-content b)) (- (right-content b) (right-outer l)))]

This is saying that if a container b has text-align: center specified, then its left gap (the x position of left outer edge of its first child f, minus the left content edge of the container b) equals its right gap (similar, using right edges, the last child l, and the subtraction being reversed). It's a constraint! But then if you go a few lines up you'll see that before actually applying this constraint, we first check if the container is even big enough to contain the content, and if not, we left align it no matter what.

What? Really? Yep. It's a tricky little quirk of the CSS semantics, section 9.4.2 of CSS 2.1.3 [3 Actually, I think the controlling standard on this exact quirk is now CSS Text Level 3 which has a quite clear paragraph documenting this behavior.] If text is centered inside a box too small to contain it, we don't want it spilling out the left edge (it might go off-screen, where the user cannot scroll); left-aligning ensures it only spills out on the right.

That's a funky quirk but also, you may have never noticed it and if you did this edge case probably was better than what the layout would have been. Meaning, actually, building this edge case into the definition of text-align was a smart choice by the CSS designers, embedding hard-earned design wisdom into intuitive rules that people mostly use without issue. (text-align is not considered one of the bad scary parts of CSS.) And on the contrary, in a "clean" constraint-based system, web page designers would probably not bothered to manually add this as a constraint, and probably in quite a few cases that would result in worse, not better layout.

Generalizing a bit, the challenge is that we're never just "deciding what our page looks like". We are always designing a layout that is responsive to parameters like screen size, zoom level, details of font rendering (Windows and macOS render identical fonts slightly differently), operating system details, and even higher-level changes in our application like new content, new features, translations to other languages,4 [4 German words are very long, Chinese ones are very short.] device oddities like notches, and so on.

Designing a layout from scratch that looks good in any possible one of those contexts is basically impossible—it's hard enough to do both desktop and mobile!—and so designers are, by necessity, going to rely on implicit knowledge encoded somewhere on what to do in edge cases. There's going to be a huge amount of this implicit knowledge, and whether it's encoded in rules or weights or optimization criteria it's going to be opaque to designers and surprising at least sometimes.

For example, in CSS you can also justify text, stretching spaces between words so that all lines in a paragraph (except the last!) have the same right edge. But, famously, if the line width is too narrow and the line contains too few words that are too long, then the spaces between them get stretched comically far apart and it looks terrible. You can do better by enabling hyphenation (which might turn 2 really long words in a line into 3 or more moderate size word chunks) or letter-spacing (which might also stretch the spaces between letters slightly) but those are themselves unpredictable and language-dependent and still sometimes look ugly.

So where did all these implicit rules about justification come from? Well, text justification comes from a long Western tradition.5 [5 That post claims Trajan's column, built 113 AD, as an early example.] So what happened in that long Western tradition, before CSS and computers, when this problem came up? Well, in the olden days, if you were a newspaper columnist and your column was ugly when justified6 [6 Most newspapers justified their text and also ran it in lots of narrow columns, so it was especially a problem with newspapers.] then your editor might just rephrase your text into smaller words. That technology might be slowly becoming possible but is clearly outside the bounds of what CSS would do. Generalizing, these implicit rules often draw from traditions where these edge cases simply never came up! So it's no surprise that there might be no ideal workable of implicit rules with no edge cases.

So what can we do instead?

I think what we can do, though, rather than trying a ground-up re-conception of the problem, is to improve the situation by providing more-intuitive rule systems with more-predictable less-esoteric rules. For example, when designing CSS layouts, you can use negative margins and floats and clear: both like we did in the A List Apart days. Or you can use flex-box and grid. Both are workable but you can guess which of these I teach to students!

To analogize a bit… JavaScript has all sorts of bizarre mis-behaviors and semantic oddities like for ... in loops or the with statement or totally insane semantics and syntax7 [7 Look for "Direct and indirect eval"; eval(<expr>) is a special syntactic form separate from function application but there's also a function named eval that can be applied with normal function application, and they behave differently.] One solution to this problem is to forsake JavaScript forever, to use Lisp or Haskell or Rust or something. But another is, like, Typescript and ESLint, which together make it really easy to avoid all the bad features of JavaScript and use good versions instead. C has a similar situation, where strcpy has really simple but also bad semantics. One option is forsaking C and rewriting in Rust. Another is using strlcpy.

In CSS specifically, a lot of the problems in CSS layout are problems more precisely in "Flow layout", the default layout mode which is optimized around layout out text in a standard Western style. What's fine for text isn't good for applications, and doing UI design using text formatting tools like floats and clear was never going to be simple and intuitive. By contrast, newer layout modes like Flex-box and Grid are maybe a bit more complex off the bat, but once you grasp the mental model are quite intuitive with far fewer sharp edges.

So I think in fact the solution is what the CSS committee has been doing, standardizing new and more intuitive layout modes optimized for the specific types of layouts being poorly served by what currently exists. With just a bit of effort, those new layout modes can be intuitive, with few edge cases, while still building in a lot of implicit knowledge around, for example, how to respond to container size changes or handle content that is too big or too small.

Thanks to my PhD student Andrew Riachi, whose work on his own website inspired this blog post.

Footnotes:

1

Naturally in both cases you actually compute a lot more than just sizes and positions: fonts, colors, and so on. But layout is taken to be the "hard part" of the problem, and I don't really disagree with that.

2

Newer students seem really excited about Typst, which I have not tried but.

3

Actually, I think the controlling standard on this exact quirk is now CSS Text Level 3 which has a quite clear paragraph documenting this behavior.

4

German words are very long, Chinese ones are very short.

5

That post claims Trajan's column, built 113 AD, as an early example.

6

Most newspapers justified their text and also ran it in lots of narrow columns, so it was especially a problem with newspapers.

7

Look for "Direct and indirect eval"; eval(<expr>) is a special syntactic form separate from function application but there's also a function named eval that can be applied with normal function application, and they behave differently.