Componentizing the Kibana UI, Part 1: CSS that Scales
Here be CSS at scale
Tens of thousands of lines of CSS. Selectors nested eight levels deep. Override upon override upon override until… (gasp)… !important
rears its ugly head! Some may cower and tremble at the very mention of such beasts, but here on the Kibana team, we meet these foes head on, with bellyfuls of coffee and web inspectors at the ready.
Am I being too dramatic? Perhaps. But the struggle of writing scalable CSS is real. On the Kibana team, we’re tackling this problem with a formalized approach towards writing CSS:
- We think about our UI in terms of components.
- We use classes to simplify our CSS.
- We use the BEM naming convention to make our markup and CSS more predictable and readable.
We build our UI out of components
Components are the building blocks of a user interface. They’re the buttons, form fields, modal windows, and other visual elements which engineers glue together in the process of building out a feature.
By writing CSS in the form of components, we make our engineers’ lives easier, because they can reach for existing CSS and markup instead of having to write it themselves.
CSS that’s written around components is also easier to maintain, because a component naturally limits the scope of its own styles, preventing any accidental side effects when we make changes to them.
Here’s an example of one of our components. We call it a Panel.
Components have both a visual representation and a representation in the code, and they’re both identified by the same name. This lets us talk about UI with a common language. Now, whenever a designer, engineer, or product manager refers to a “Panel”, everybody else in the conversation knows exactly what they’re talking about.
Next, let’s explore how we keep our CSS simple, then take a look at the CSS behind our Panel component.
Simplicity is the foundation of scalable CSS
Simple code has fewer moving parts and clearly expresses intent. The units of logic within it are uncoupled from one another, have explicit and unambiguous interactions, and have well-defined boundaries.
Simple CSS possesses these same qualities, but the methods with which we simplify CSS are different than those we would use to simplify code written in imperative languages such as JavaScript or Python. On the Kibana team, we’ve found that the most powerful tool in our simplicity toolbox is the CSS class.
Here are the general rules on how we use classes to simplify our CSS:
Use classes to explicitly map markup to the rendered UI. When we name classes with components in mind, the class names become enriched with meaning. These class names act like landmarks and road-signs in our markup. They help us navigate the markup, understand the relationships between elements, and form a mental image of the UI without having to constantly check the browser.
Limit selectors to a single class. When a selector involves more than one class, it becomes dependent upon context. This means that its styles require a specific relationship among elements to take effect, and this relationship can be difficult to spot when reading the markup. By limiting each selector to a single class, we simplify the relationships between elements, making our markup easier to read and reducing the probability that we’ll accidentally break something when we change the context.
Only use inherited properties in “leaf” nodes. When CSS applies properties such as “font-size” and “color” to an element, it also applies them to the element’s children via inheritance. These side effects can sometimes be unexpected and even undesirable. Our solution is to prevent inheritance from driving the appearance of our UI by only using inherited properties for those parts of our UI which won’t contain children, e.g. form labels and buttons.
Let’s look at how these principles apply to the code for our Panel component. Here’s the markup for creating a Panel:
<div class="panel">
<div class="panelHeader">
<div class="panelHeader__title">
Panel title
</div>
</div>
<div class="panelBody">
<!-- Content goes here -->
</div>
</div>
Notice how the UI is clearly identified by the use of class names in the markup. It’s obvious that this is a Panel component containing a header and a body. On the Kibana team, we’ve found that by improving our markup’s readability, we’re also able to build UIs more quickly and reliably.
Here’s the CSS that defines the appearance of this markup:
.panel {
border-left: 2px solid #e4e4e4;
border-right: 2px solid #e4e4e4;
border-bottom: 2px solid #e4e4e4;
}
.panelHeader {
display: flex;
align-items: center;
padding: 10px;
height: 50px;
background-color: #e4e4e4;
}
/*
* FYI, we indent child classes like this to emphasize its role
* in the markup as a tightly-coupled child of the .panel class.
*/
.panelHeader__title {
font-size: 18px;
line-height: 1.5;
}
.panelBody {
padding: 10px;
}
Check out how we’ve declared each selector with a single class. This makes it really easy for us to understand how the CSS acts upon the markup, and results in a visual UI component.
Lastly, notice that the only selector which has inherited properties is the .panelHeader__title
class. That’s because we know the element to which this selector will be applied will never contain any children beyond the text in the Panel’s title. This is an example of how we sidestep the problems that CSS’s inheritance model can introduce.
The BEM naming convention
The BEM naming convention has greatly grown in popularity since it was first developed by Yandex in 2007. Explaining the nuances of BEM is beyond the scope of this blog post, so if you’re interested, I recommend reading more about it in this article by Harry Roberts, or in this CSS-Tricks article.
We chose BEM as our CSS naming convention because we needed a way to identify cohesion between the different classes that comprise a component when we read our markup. At the same time, we need a way to differentiate the role each class plays within our markup -- some classes play a structural role, some classes modify others, and some are meant to be applied dynamically via JavaScript. BEM provides us with patterns to tell these different classes apart.
For more on how BEM works, check out the links I mentioned, above.
Next up: Writing JavaScript with React
This is part 1 of a multi-part series. Take a look at part 2, "Writing JavaScript with React", to learn how we use React to design components for reusability and composability. Thanks for reading!
And if you're interested in joining our team, we're interested in hearing from you!