There is no box model in CSS — and this is why borders are terrible
JavaScriptUI — DevBlog #7
“C’mon man, don’t be ridiculous. Everybody knows that the Box Model is the foundation of CSS. What the hell are you talking about?”
Well, I hate to be that guy, but this is a box:
And this is a rectangle:
Well, more like several rectangles, but definitely not a box. Sure, calling it “The Rectangle Model” doesn’t quite have the same ring to it, but it would make things a lot easier.
See, if you take a closer look you might notice a striking difference between a box and a rectangle: one is three dimensional and the other is completely flat. When you call something a “box”, people will naturally expect something three dimensional, yet CSS goes above and beyond to flatten everything into two dimensions.
This dissonance has led to far reaching consequences in terms of building web layouts and user interfaces. One of them is the inability to apply proper borders in CSS due to the fact that they are flattened into the document. This alone demonstrates why I think the CSS “box” model is fundamentally broken.
Let’s see why.
The Three Musketeers
At the heart of the CSS “box” model, there are three concepts: the margin “box”, the border “box” and the padding “box”. Seems simple, right? Well, not quite. The problem is, in CSS, the border is always part of the layout, even though it should’t be. This creates two significant problems:
- the size of any element becomes ambiguous
- the layout shifts whenever the width of any border changes
To solve the first issue, CSS introduced the box-sizing
property. If we set it to border-box
, the element size includes the border width and if we set it to content-box
, it doesn’t. Problem solved, right?
Wrong again. Maybe this is just me nitpicking, but in this model there is no such thing as a content-box
. We have margin “box”, border “box” and padding “box”. The padding “box” already conveys what we want (the size that excludes the border but includes padding), so I don’t really understand why we need to call this a content-box
instead. If anything, I would intuitively think that this is the area within the padding “box” (the fourth “box”).
Nevertheless, we still have the second issue: layout shifting. Regardless of box-sizing
, your layout will still shift. You just have the option of whether to mess up everything inside your “box” or everything outside of it. Take a look at this simple example where we add borders to a button on hover:
The code to achieve this is fairly simple:
Stack(
Button(`Border "box"`)
.css("box-sizing", "border-box")
.width(150)
.height(50)
.cornerRadius(25)
.css("text-align", "center") //horizontal align
.css("line-height", "50px") //vertical align trick
.backgroundColor("#027FFC")
.onMouseOver(self => self.border("2px solid black"))
.onMouseOut(self => self.border("none")),
Button(`Content "box"`)
.css("box-sizing", "content-box")
.width(150)
.height(50)
.cornerRadius(25)
.css("text-align", "center") //horizontal align
.css("line-height", "50px") //vertical align trick
.backgroundColor("#027FFC")
.onMouseOver(self => self.border("2px solid black"))
.onMouseOut(self => self.border("none"))
)
.width(680)
.height(500)
.gapX(20)
.css("justify-content", "center") //horizontal align
.css("align-items", "center"); //vertical align
Note: I won’t go into detail how nonsensical horizontal and vertical alignments are in CSS, but these will definitely be fixed in JavaScriptUI. For now, I am using basic CSS tricks.
This is a common and fairly simple layout situation: we create two buttons with equal size and we add a 2px wide solid black border on hover. If we use border-box
, the layout breaks inside the button and if we use content-box
the layout breaks all around it. I cannot find a more basic example than this and even here the “box” model completely breaks down.
You might trivialize this issue saying that it is just pixel f*cking, but this is a significant accessibility problem for people with visual impairments or cognitive disabilities. And this issue is prevalent everywhere we set any kind of border on any kind of element dynamically.
Tricks, hacks, but no real solution
Unfortunately, there is no good solution for this problem. There are a ton of clever tricks and dirty hacks, but none of them work without significant drawbacks:
- adding a placeholder border to the idle state that uses the same styling as the background and toggle all these styles on the border instead of toggling the existence of the border itself: this destroys the semantic nature of our code, forces us to change multiple values instead of a single
border
property and also becomes increasingly complicated as the styling complexity of the background increases (opacity, backdrop filters, more states than idle and hover, etc.) and we cannot achieve centered borders this way (we will get to this later) (also, what’s with centering with CSS, why is it so terrible almost everywhere?) - faking idle borders using
padding
: non-semantic as well, have to toggle multiple values (disable padding, enable border and vice versa) instead of a single border property, no centered “border” (inset or outset only) - faking borders using
outline
: again, non-semantic solution, uses a completely different tool for the job that already has a purpose and makes our code increasingly confusing, no centered “border” (inset or outset only) - faking borders using
drop-shadow
: same issues, non-semantic, has a different purpose, ugly, no centered “border” (inset or outset only) - adding another element or an svg rectangle on top of our regular element that acts as the border: non-semantic, bloats not just our styling but our view tree (html hierarchy) as well, terrible to work with (eg. rounded corners?), in general it is a pain to overlay elements in CSS (the very issue that breaks the “box” in the box model), but at least it can be centered, not just inset or outset
- using relative instead of explicit sizes: cannot match the widths of two or more buttons, always behaves like content-box, thus everything grows outward (cannot contain the layout shifting issue within the button anymore), still no centered border (outset only)
No matter what we try, the “solution” will be ugly, unnatural and cumbersome. If something this easy is this miserable, how can we expect to build anything worthwhile efficiently? And keep in mind that this is the very core of CSS.
Let’s recap
We have the box model, which is actually a rectangle model, we have the content-box
, which is actually the padding box and we cannot add borders without messing up the layout. We are forced to use placeholder borders that match the style of the background, fake borders using padding
, outline
, or drop-shadow
or use another overlapping element just to work around this fundamental issue. And even then, we still cannot get a centered border, only inset or outset. And you might wonder why CSS has a steep learning curve.
A true solution from an alternate dimension
If the default CSS “box” model is so bad then what would be the correct approach?
Easy: just remove the border from the “box” model and place it on top of our element. There is absolutely no need to flatten borders into the same plane along with our content, margins and paddings. We should simply utilize the third dimension like how any sane visual design software works:
In this model, even if our border (stroke) is set to inset/inside or outset/outside, it is rendered on a different layer on top of our actual content. Borders do not and should not affect the layout whatsoever.
The result
Using this approach, we get a single, unambiguous element size, we can change the border in any way we like without affecting the layout and we can even align them properly. Suddenly, all hacks, all unnecessary concepts such as box-sizing
and content-box
are completely removed from our workflow and now even the name Box Model makes a little bit more sense.
But one huge extra benefit on top of all this is that now the design (especially graphic design) and web developer teams would speak the same language. No friction, no back and forth. The web would finally work the same as everything else, instead of reinventing the wheel at every possible turn.
Conclusion
The term CSS “box” model is incorrect and misleading because it suggests that we can work in three dimensions, even though achieving even an overlaying border is practically impossible. All possible workarounds are extremely limited and cause significantly more problems than they solve. Removing the border from web layouts would significantly improve working with them as well as with the layout itself, and it would reduce the gap between graphic design and web development. Unfortunately, I haven’t been able to find an acceptable solution in JavaScriptUI, but if you think of a solution, please share it in the comment section.
The implementation of borders is by far not the only issue that breaks the CSS “box” model. In general, anything that requires overlays, overlaps or separate layers (basically anything that is widely used in graphic design) go against this model and will be extremely ugly and cumbersome to achieve. I will discuss several basic examples just like this in further posts.
Until then, thank you for reading my post and if you like what I’m doing please consider clapping, commenting and sharing my stuff with others.
⬅️ DevBlog #6 — 3 options to handle vendor-prefixed CSS properties in JS