Frankenstein's display property
UIScript DevBlog, Week 11
If you’ve ever struggled to understand the display
property in CSS, especially in combination with the position
and/or visibility
property then this article is for you. I will explain why this property is badly designed, what problems it is trying to solve and how we could solve them in a much better way instead.
Multiple personality disorder
It is a common theme in the design of CSS that single properties are trying to do the job of several ones. This is the case with the display
property as well. Generally speaking, the display
property is used to do the following things:
- Control the visibility of an element
- Control the layout of an element's children
- Control the layout of the element itself
- Control the layout of both an element and its children at the same time
These are separate responsibilities that should be handled with separate properties, yet CSS attempted to “simplify” things by pushing all this functionality into a single property. So let’s deconstruct the display
property and examine these one by one.
Visibility
Controlling the visibility of an element is probably the weirdest responsibility of the display
property and it cannot even do that properly. When you set the display
of an element to none
, that element loses its real display value so you cannot simply toggle that value back:
let div = document.createElement("div");
div.style.display = "flex";
div.style.display = "none";
div.style.display = "flex"; //how to toggle this back without setting it to flex again?
When I first stumbled upon this mess I thought that the revert
global value is the solution but as always, this is just a bad name for a value that doesn’t revert anything but instead resets the property to its inherited value or to the default value if the value cannot be inherited. As to why this global value isn’t called reset
then, is anyone’s guess.
“But bro, you have to use the visibility
property for these things. Everybody knows that.” Well, this is not exactly true. The visibility
property cannot remove an element from the view tree, it just makes it invisible. The element still takes up space, it still affects the layout but it is completely transparent.
“Then why do we need the visibility
property in addition to the opacity
property?” Well, because the visibility
property also disables pointer events on the element, duh. You know, something that is controlled by the pointer-events
CSS property, but who cares, right? If the display
property can have multiple personalities, so can any other CSS property.
Controlling the layout of an element’s children
This responsibility is probably the easiest to understand and the closest to what I think this property should do. Instead of manually laying out elements one by one, we use containers: parent elements that have a predefined organizing logic to lay out their children automatically. This organizing logic is then set by the display
property.
There are only a handful of values that work more or less as clean container layout modes. One such example is flex
. When you set display: flex
on a container, it lays out all of its children in a vertical or horizontal line.
<div style="display: flex">
<div></div>
<div></div>
</div>
Controlling the layout of the element itself
This is where things get really confusing. There are container layout modes that do not work without setting the layout mode on its child elements as well. One example is the static
value. When we set a parent element to display: static
it needs to know which child element is inline
and which is block
in order to be able to lay them out automatically. Sadly, this is also set by the same display
property:
<div style="display: static">
<div style="display: inline"></div>
<div style="display: block"></div>
</div>
Wonderful, isn’t it? This is not only confusing and idiotic, but also self limiting as we will see in the next chapter.
Note: As always, I have to point out how idiotic it is to call an automatic layout mode “static”. What this value describes is the exact opposite of being static.
Controlling the layout of both an element and its children
The problem with setting the layout of an element and the layout of its child elements with the same property is that these two will inevitably clash. What do you do when you need to set an element to inline-block
to work inside a static
container, but you also need to lay out its children like a flex
container? You have a single property but two values at the same time:
<div style="display: static">
<div style="display: inline-block"><!-- this is a flex container too, but how? -->
<div></div>
<div></div>
</div>
</div>
As always, instead of taking a step back, understanding the root cause of this problem and solving it once and for all, CSS introduced yet another concept: precomposed keywords. So instead of simply introducing a separate property, it introduced compound values for the display
property, like inline-block
, inline-table
, inline-flex
and inline-grid
.
Obviously, this wasn’t enough, so CSS went all in and introduced yet another concept: inside and outside display
values. Again, keeping a single display property but with two separate values like inline flow-root
, inline table
, inline flow
and inline grid
.
Note: Outside and inside are also terrible names and they make it very hard to understand what they are actually doing. Outside means defining the layout of the element itself, while inside means defining the layout of the element’s children.
It is anyone’s guess when to use which value type, but I am sure there are thousands of pages of specifications to make it even more complicated still.
Let’s recap
The CSS display
property is used to do several completely different things. It is used to toggle the visibility of an element, even though this would be the responsibility of the visibility
property, which in turn does what the opacity
and the pointer-events
properties should do. But it cannot even toggle the visibility properly: it can only disable the visibility without keeping the previous layout mode. To fix this, we cannot use the global revert
value in CSS, because it doesn’t actually revert anything, but resets this property instead. It is also used to control the layout of child elements like a container, but it breaks down once we use a container layout that needs individual settings for its children (like in static, which isn’t actually static), because an element can act both as a content and a container at the same time. To “fix” this, CSS introduced the concept of precomposed keywords, then the concept of outside and inside display values that do not actually refer to outside and inside of an element, but rather to the layout of the element versus the layout of its children instead.
bUt CsS hAs A sTeEp LeArNiNg CuRvE.
Yeah, right.
The solution
So finally, let’s see how UIScript cleans up this mess. First, we introduce a proper way to toggle visibility like so:
let view = Div().visibile(true);
view.visibile(false);
That’s it. When you enable/disable the visibility of an element you don’t change its type, layout mode or any other layout or styling feature. You just enable/disable its visibility.
There is, however, another option, which is impossible using only HTML and CSS, and that is adding and removing a view to and from the actual view tree. This should have the same effect, we are just a bit more procedural in our approach:
let view = Div();
App(
view
);
view.remove();
To understand this a bit better, consider the following vanilla JS code (UIScript is doing the exact same thing under the hood):
let view = document.createElement("div");
document.body.append(view);
view.remove();
Second, we introduce proper container types instead of handling the layout modes through a property. This makes UIScript a semantic layout system: it stores layout information directly in its types. Here are some basic containers that UIScript provides:
Group(); //defines a generic container without a layout mode
Text(); //defines a block of text
Stack(); //defines a linear distribution for views (uses flex under the hood)
And here are some of the more advanced containers in UIScript at the moment:
Options(); //contains the built-in option and optgroup views
Table(); //contains rows or columns that contain cells
Note: We could, in theory, handle the container layout mode using a view property instead of a view type, but it is hard to imagine a situation where we would need to switch one container to another (eg. Text to Stack), it just doesn’t make sense with proper containers. But if, for some reason, this is absolutely needed, we still have the option to create the new container and migrate all children from the previous container manually.
There are three key takeaways here.
First, we only define containers that have homogeneous children, so no inline
, block
and inline-block
nonsense. This also eliminates the problem of setting a layout mode for the element itself and for its children at the same time, so no “compound-precomposed-super-duper-double-outside-inside” bullshit.
Second, we disallow children to modify their layout in any way whatsoever. In CSS, when we set the position
property on a children, it is essentially removed not just from the layout set by its container but also from the parent itself, if the parent isn’t positioned as well. Positioning elements manually is just one of these container modes and it is exactly how the Group()
container is supposed to work. For more information you can check out my article about position: fixed
here.
And third, we are free to introduce and/or build any kind of containers we like. We can build on top of these basic containers or go wild and implement whatever crazy idea we need in our projects. The choice is yours, it is returned back to the developers from the gatekeeper committee overlords.
That’s a wrap
That’s all I have for you this week, please consider clapping, commenting and sharing my work with others if you enjoy what I am doing. It really means a lot.
Thank you and have a wonderful week.
⬅️ Week 10 — Special Edition — WeAreDevelopers, New Brand, Same Vision