It is not a secret that I love CSS. A few years ago I fell in love with a very simple, but powerful CSS selector. Back then I was expanding my CSS to the next level. I knew about the specificity and the cascade. I had no issue using CSS from scratch or with a framework. But I came across new CSS selectors for complex solutions. So I had to expand my knowledge on CSS.
I started with finding resources online, like Smashing Magazine. There I came across Heydon Pickering and later his 'lobotomized owl selector' of . This selector blew my mind. At the CSS Day, he even showed another beauty, called the 'flexbox holy Albatros' (you can watch it here). These types of solutions showed me that CSS is a lot more powerful than I knew. Solving solutions in CSS can be easy or elegant. So, Heydon, this one is (partly) for you.
"Solving solutions in CSS can be easy or elegant."
The lobotomized owl selector
Heydon explains the selector better in his article than I can. But I will provide a quick summary. The selector is, as mentioned, very simple: * + *
. The *
is the universal selector in CSS, it applies to all elements in the DOM. The +
is the real hero of this piece of CSS code. It has the beautiful name of 'adjacent sibling combinator'. It applies the defined styles to the second element if immediately follows the first element. With our selector, it applies styles to all non-first elements on the same level in de DOM. Unless other rules have a higher specificity.
* + * {
margin-top: 1.5rem;
}
So why is this selector so powerful? I found the selector when searching for a spacing solution for a blogging website. I wanted to create enough space between paragraphs, but also to their parent element. Many solutions exist to solve this. You can give every element a margin-bottom
. This has a side effect on the last element. To solve this, override the styles with the :last-of-type
pseudo-selector. Another solution is to add both a padding-top
and padding-bottom
to each paragraph. This can create unwanted side-effects with paddings of the parent element. More in-depth description of this specific problem and solution can be found on Every Layout, which is a brilliant website.
The * + *
seems to be an elegant solution when using margin-top
. The margin-top
is only applied between the elements. But you can also have images using img
or svg
elements which have different spacing. Then try something like p + p
. You can make this structure as specific as you want, ensuring a simple and elegant solution. But you know what makes it powerful? In this setup, it works on nested elements!
As you can see on the left, the margin-top
rule applies to every element in the list. On the right, the second element gets not only the styling rule, but it also has two child elements. Of those child elements, the second one also gets the margin.
Algorithms in UI
You might wonder why I am so obsessed with this little CSS selector? In the web development industry, and at the 2019 CSS Days, there is a reoccurring subject. Is CSS a programming language? For the answer to this question, I recommend the talk of Lara Schenck, which you can find here. This talk got me thinking about the state of CSS and how I use it myself. How complex is it for the browser to transform my CSS commands into something shown on the screen?
The owl selector is a wonderful example showing how complex parsing CSS selectors can be. It shows the real power of CSS. Why? Because it works nested. In any programming language, this can become complex fast. The nested nature of the selector can is comparable to recursion. Before diving into the recursive solution, let's look at the pseudo-code for a flat list of elements.
Disclaimer: the code examples used do not work in any programming language, they are pseudo-code snippets.
function owl(list, apply) {
for (i = 1; i < list.length; i++) {
apply(list[i]);
}
}
Our function gets a list of elements and an apply
function as input. As you can see, we start at 1
. Not because arrays start at 1 (they do not), but because our CSS selector skips the first element by default. The function works like * + *
on a non-nested list. The callback apply
is used to any element in the list. But what if we want to have something like p + p
or even img + p
? We have to add checks to ensure the adjacent elements follow the definition.
function isFirst(item) { ... }
function isSecond(item) { ... }
function owl(list, apply) {
for (i = 1; i < list.length; i++) {
if (isFirst(list[i]) && isSecond(list[i - 1)) {
apply(list[i]);
}
}
}
With the function, we can work with lists of varying types of elements and check if adjacent elements fit our criteria. We are only lacking the nesting capabilities of our CSS selector. We could check if our element has any children. If so, we call the owl
function again. Yet, in HTML any element can have children. This means that even our elements complying with our adjacent rule can have children. So instead of calling the owl
function if our element has children, we first have to call the apply
function. So it can happen that for a single element, both apply
and owl
are called.
function isFirst(item) { ... }
function isSecond(item) { ... }
function hasChildren(item) { ... }
function owl(list, apply) {
for (i = 1; i < list.length; i++) {
if (isFirst(list[i]) && isSecond(list[i - 1)) {
apply(list[i]);
}
if (hasChildren(list[i]) {
owl(list[i], apply);
}
}
}
You now know how to create something like our owl selector. It exists of a recursive function with some extra functions to check the conditions. The above pseudo-code can become more complex if our CSS becomes more complex. Try to combine it with different pseudo-selectors, or change its specificity. By doing so, you will see how powerful CSS has become.
Is CSS a programming language?
As I mentioned, during the CSS Days one of the reoccurring topics was "is CSS a programming language?". Almost everybody can apply simple CSS rules or styling rules. Solving more complex (or even easy) problems require more in-depth knowledge. Knowledge of computer science concepts becomes important, as they are the result of CSS rules.
"Knowledge of computer science concepts becomes important, as they are the result of CSS rules."
Kevin Pennekamp
A simple CSS selector can mean that you apply a recursive function. This is nothing else than using a function written by someone else. The mental model of the result remains the same. You are applying algorithms to create a UI. This is exactly the reason I love CSS. Something simple can become a powerful UI manipulation tool. Every time I find solutions in CSS that I deemed not possible. So I want to thank Heydon Pickering, Lara Schenck and all the others that showed me the real power of CSS.
This article was originally posted on kevtiq.co
Top comments (12)
Regarding the performance, the Owl selector uses the
+
that will search for the adjacent element. So, the second universal selector*
will not hit performance. It would have the same performance of a single universal selector.I always use it together with a
>
that will search only for the first child, so it impact less performance.So, for example, to add a left-margin for all the items (except the first) in a row:
There is a good explanation about performance here: github.com/stylelint/stylelint/iss...
You have no idea how well timed your comment is for me ;). I had a colleague asking me about its performance just af few days ago!
I agree the usage with the
>
or at least a specific class before it that you know it is not extremely nested.Cool, great to know that it helped!
I learned it when I was trying to understand why stylelint complains about having two universal selectors together. Then I found this link: github.com/stylelint/stylelint/iss...
Is complexity a good thing though? I personally would refrain from using something like the owl selector not just because of performance reasons, but because it is not revealing your intention at all. It's mentally difficult to parse. From my point of view, it falls into the "too clever" category.
I understand the point to show that CSS is powerful, it certainly is, but it can also be extremely unpredictable at times. At least for me, CSS involves a lot of trial and error, partially because rule specifity keeps hitting me in all the wrong ways. My biggest problem with CSS however is that it essentially attempts to solve a nearly unsolvable problem: global theming versus independent isolated component styles. Those two things will always conflict no matter how smart your CSS setup is. I've never seen a good solution.
But how different is it to apply the owl selector to achieve a result, or use a function from an npm package of which many do not look what is actually done? In both cases, the intention can become blurry, if applied without thought. Anyone without knowledge about recursion or creating a recursive himself/herself can easily apply such a function to solve a problem. In other programming languages abstractions/interfaces can also quickly lead to blurry intentions, while adding complexity. I;ve seen examples from colleagues that they found amazing, and I also put in the 'too clever' corner.
I dont believe that CSS was made to solve this nearly unsolvable problem. It is is definitely a problem for many developers working with CSS. But I cannot help but feel that it is a problem we have created ourselves as a result of the modern front-end frameworks (we want isolated components), not because of what CSS wants to be. In the end, all components are rendered together on the screen, and one component can influence a different one due to its positioning. So can we really say that the component is independent? Or is it just a mental model we developers want to achieve, to scope our CSS like we scope features?
We undervalue what CSS can do, so CSS has to adapt to our frameworks, instead of we learning how to really use CSS. Many see CSS for styling, but it is a lot more than that. CSS these days is the solution for solving layout problems. And when you work with designers and various screens, these problems can become rather complex. Complex problems often require more complex solutions. This one of the reasons why the owl selector, or actually the 'adjacent sibling combinator', was added to CSS.
On the notation of specificity: I think when applying CSS selectors with trial and error would make it more difficult to understand why CSS act like it does. But there are CSS architectures that could definitely help when applied correctly, that work well with the specificity. BEM, OOCSS and ITCSS are some examples that are solid and scalable architectures to setup CSS in big projects.
I find your view very interesting and definitely respect it. So if I come in strongly, my apologies!
For me the biggest step forward (which was terribly long overdue) for CSS was the addition of flexbox and (more recently) grid layouts. I know, bootstrap had grids since forever, but really understanding that CSS code was hard, at least for me. Since I have flexbox and grids, even I (primarily a backend dev) can create decent layouts in reasonable time, without the need to resort to... arcane combinations of
position
and text flow tricks. So that's definitly a big improvement.As for component isolation: if you do everything on your own, or use an off-the-shelf solution (e.g. react material UI) where everything is designed to work and play nicely together, having global CSS rules is a blessing, no questions asked.
However, if you need to combine e.g. react components from various different sources, then the global nature of CSS - at least in my experience - can become a hindrance, rather than a help. Of course I want the recently added dropdown to look like a dropdown. I want easy integration after all, not just a mess of
<div>
s. Then again, I want it to fit into my app theme - margins, colors, those things. And that's when things can get really messy really fast, in particular if the author of the component you just added did a quick-and-dirty styling job (alas, "quick-and-dirty" applies not only to styling of many react components out there in my experience). If you are truly unlucky, then the component uses inline styles (which always win the specifictiy contest). Sure, it will look the same everywhere - but that's a double-edged sword. Remember: you want the component to fit in. You don't want material-style shadows and ripples in an otherwise flat UI.I really hope that the movement started by react (towards reusable components and a big community-driven library for everything you could possibly want) will continue. I just don't see at the moment how CSS will keep up with that. Doing both isolation (I provide a component which everyone can use) and global styling (I want my app consisting of components from various sources to follow coroprate identity) seems to be nearly impossible. But maybe that's just the rambling of a backend dev who gets to touch CSS once in a blue moon ;-)
Thanks for the pointers, looks interesting from a first glance. And no worries, no offense taken at all.
Solid examples you provided when components can make your life miserable when it comes to CSS. You are completely right when it comes to importing components from other libraries that ship a lot of their own CSS. Seen those sometimes, but I really try to avoid them, unless I really cannot do so anymore. In most cases I feel that those components are wrongly designed. Applying your styles properly should always be possible. In my case, I am there to have your feature set, not your styles, as they hardly ever match. The bare minimum that you can ship is CSS that is require to let the component function like it should (e.g. make a table side-scrollable, while looking ugly as hell).
Take for instance a table component. In most cases you can set a lot of Props in the React component to ensure that everything is working in various configurations (headers, pagination, multi-select etc. etc.). But when everything is shipped in one component, it is difficult to ensure you can apply styles correctly. It is far more beneficial if the library allows to build components like:
In this case you have the ability to apply your own styles on 4 different component (and wrap everything once yourself, creating your own styled tablewrapper component). Another example could be a Popup. It mainly consists of a click-area, and content that popups from an indicated direction. Instead of providing one
<Popup />
component, provide something like shown below. This again allows to provide more or your own styling. The only styling you need to ship is to ensure that the content is positioned correctly, either on click or on hover. But the content itself can just be something with styles (e.g. background, spacing, drop-shadows).So most of the issues we come across when using other people components, especially isolated (e.g. only a dropdown from material-ui), are components designed without other peoples CSS-needs in mind.
(This discussion inspired me for another blog-post I can write, thanks!)
Thanks for the in-depth answer!
Minimal CSS is good - I think we can all agree on that. But when exactly do you stop? After all, people will often download components based on screenshots. Your component may be good, and with carefully crafted minimal CSS, but will it receive any attention if it looks ugly? On a similar note, what if I would rather have a component which looks good by default and does not fit in as well, but is easy to just slap into an app? My point here is: I think that (just like "good API design") "minimal CSS" is a lot easier said than done.
The worst CSS experience I've ever had (which just happened to be my first contact with CSS ever) was with a big UI framework I had to deal with (I won't tell the name, it's still alive), which had a really complex SCSS theming going on, poorly documented of course, and to add insult to injury the HTML structure was generated dynamically at runtime, with DOM modifications galore on top. You had to (more or less blindfolded) guess the right selector to match the generated structure, and then pray that it happened to be more specific than any of the extensive theming which was already in place. No hot reloading by the way, a server restart was required for every change. It was a nightmare. I still clearly remember the 4 hours of work it took me just to change the font color of a tabsheet heading. So I'm a little biased when it comes to CSS, but I've also had some positive experiences with it later on.
I remember a similar experience. I learned CSS before the big frameworks existed. But at a certain point it got to work with a rapid development tool (kind of drag and drop). This tool did allow to create your own theme in scss. But it shipped its own scss framework as well, on top or one of the bigger css frameworks. I never apply some much selector nesting and !important to override the styles. This experience made sure I don't want to rely on css frameworks again. If you do, specificity does become a trouble.
Minimal CSS and good API is definitely easier said than done. We can definitely agree on that. I think it is almost human nature that we always try find one solution for our problems. In my previous table example, I have the separate components with minimal CSS, but I also have a fully styles wrapper (one component and one scss file). It's an ideal way to show how it can be styled.
This is a mental model issue for us as developers. We are keen so share our work and for others to use our work. But this is relatively new in front-end land. So we forget sometimes to think of potential use cases of others (styling wise, feature wise we do it quite alright). In back-end development it is already more matured (e.g. REST API design).
* + *
That's all elements including head and body and we'll everything the find the next sibling of any kind. Despite the facet that the initial * is really slow. I better read this article in full because I'm missing the point at the moment.
Regardless of selector performance, I tried to show how such a simple selector can be very powerful, due to it being applied nested. Many other selectors also apply nested, but as they do not rely on having siblings, you could do just a search throughout. Because this selector relies on having siblings, mentally you have to recursively go through the DOM tree to determine the effect of this selector. Recursion is a concept that is sometimes deemed difficult by (starting) developers. Such a simple CSS selector applies a powerful computer science concept, without us even knowing it.
tl;dr:
Its a simple selector that applies a powerful computer science concept, showing the complexity of CSS.
I enjoy a lot the owl-selector, but recently I realized that it could be replaced (in many cases) by a flexbox with row/column gap. At least for me, it works for most of my cases.
So, this:
Could be replaced by
In this case, all the items of the row will have a spacing of 12px among them. Even if list is broken into more lines (using
flex-wrap: wrap;
), each item starting in the below lines will not have the left-margin applied to it (they will be correctly aligned). Then, can also add a spacing among the lines: