Switching the Umbraco backoffice to utility CSS
Heads Up!
This article is several years old now, and much has happened since then, so please keep that in mind while reading it.
Let's look at the Umbraco CSS architecture.
At the core of the Umbraco backoffice is a customised version of Bootstrap. Further bespoke CSS is added on top of the framework to add extra features and functionality that don't already exist. In addition, the backoffice will load any CSS files and assets that might be requested by:
- Third-party JavaScript plugins (e.g. jQuery UI)
- Installed packages from the Umbraco community (e.g. Vendr or Contentment)
- Installed packages from Umbraco HQ (e.g. Umbraco Forms)
What are the current CSS architecture problems?
No documentation
I could not find any documentation for Bootstrap or the CSS used in Umbraco. Lack of documentation means there is no guidance for file locations, coding standards, naming conventions, or what existing classes are available.
The class naming standards vary throughout the Umbraco backoffice. Some are based on Bootstrap, others use Block Element Modifier (BEM) naming conventions, and many are either somewhere in-between or completely bespoke.
Without documentation, a backoffice contributor could:
- Create unique naming conventions
- Ignore existing standards already used by Umbraco or Bootstrap
- Introduce new colours, sizes, spacing, fonts, and more
- Duplicate existing styles, rather than reuse existing classes
- Edit a class that has an adverse effect elsewhere
A package developer could:
- Include new stylesheets
- Introduce new colours, sizes, spacing, fonts, and more
- Duplicate existing styles instead of reusing existing classes
High risk CSS changes
There is a higher risk when adding, editing, and deleting backoffice CSS classes. Each change could have unknown, adverse effects elsewhere in the backoffice or on installed packages.
Adding a new class could conflict with CSS for installed packages using the same class name. Depending on the specificity, the new class could override package styles, causing broken design or layout. Or the package could override the backoffice styles, causing unexpected bugs elsewhere in Umbraco.
Editing an existing class could cause broken designs or layouts if reused by installed packages. Other areas of the backoffice could also break. The backoffice is a large application and often only displays parts of the user interface (UI) using conditional rules. If there are no members in Umbraco, for example, a contributor will not see how their change affects the members UI as it is hidden.
Deleting an existing class is a massive worry for any UI that relies on that class. Deleting or renaming a class has the same issues as editing an existing class, except it will completely remove any styles associated with it.
CSS changes aren't breaking changes
For Umbraco, changes to the CSS aren’t considered to be breaking changes. Changing or deleting a class in the backoffice can cause a broken design or layout for installed packages that reuse the modified classes. Without breaking changes for CSS, it is harder for package developers to trust the existing CSS, and a safer approach is to add their own CSS regardless of duplication.
Class name misuse
Class names are semantic in most frameworks and methodologies. Semantic CSS describes the content rather than the HTML structure or styles being applied:
<article class="article">
<img class="article-image" src="..." alt="..." />
<h2 class="article-title">...</h2>
<p class="article-text">...</p>
<small class="article-date">...</small>
</article>
.article {
background-color: white;
padding: 1rem;
}
.article-image {
display: block;
}
.article-title {
font-size: 1.5rem;
margin-top: 1rem;
}
.article-text {
margin-top: 1rem;
}
.article-date {
color: gray;
margin-top: 1rem;
}
Naming classes based on content increases the risk of class name misuse.
Imagine a tweet that also has an image, title, text, and date. It should also have the same styles as the article. To avoid duplicating styles, the article classes could be reused for the tweet to achieve the same styling. This is class name misuse because the tweet is not an article. Using a class name that describes specific content and applying it to different content will eventually lead to loss of meaningful class names.
Harder to reuse classes
To avoid class name misuse, and promote reusability, Bootstrap and other methodologies implement generic naming for class names. Generic naming results in class names like:
- .card
- .modal
- .nav
- And so on...
But this makes classes harder to reuse.
Imagine using a generic class, because we want to share styles, called card for articles and tweets:
<!-- article -->
<article class="card">
<img class="card-image" src="..." alt="..." />
<h2 class="card-title">...</h2>
<p class="card-body">...</p>
<small class="card-date">...</small>
</article>
<!-- tweet -->
<article class="card">
<p class="card-body">...</p>
<small class="card-username">...</small>
</article>
Unlike an article, the card-image and card-title aren't required for tweets:
- Would it still look as expected without these elements?
- Should the CSS have modifiers to style spacing differently when there isn’t a title or image?
There is also a new card-username class used to display a Twitter username:
- Should a new class and styles be added to the CSS?
- Other cards, such as articles, don’t need a username. Does the username styling even belong here anymore? Or should it be moved out somehow?
- Should the card-date be reused? If so, does it need renaming as the meaning has changed?
Suddenly, the CSS has started mutating from its original usage, and it might need modifier classes for cards with or without titles, images, usernames, and more:
.card-title {
margin-top: 1rem;
}
.card-title--no-image {
margin-top: 0;
}
Harder to name new classes
As Umbraco grows with each new feature or improvement, naming classes becomes difficult and time consuming. Semantic naming is hard and generic naming is even harder. A class name needs to make sense for it to be used appropriately, ideally without class name misuse.
Eventually, it is going to become almost impossible to come up with a good, unique name for a class for new Umbraco features, without being too specific or too generic.
Ever-growing CSS codebase
An ever-growing CSS codebase adds bloat to file sizes, and larger file sizes affect the perceived performance of the Umbraco backoffice. There are many reasons why the CSS codebase could grow and become harder to maintain:
- New features that require new CSS and styling
- Changing features that introduce new or duplicate existing styles
- Redundant styles from features that have been removed or changed
- Legacy styles from older versions of Umbraco
Without CSS documentation to guide contributors through what styles exist and where they are used in the backoffice, the CSS codebase could continue to grow. There is far less risk to adding new styles in Umbraco than editing or deleting existing styles, leading to possible duplication or identical classes with different names.
What are the options for improving the CSS architecture?
There are several options available for consideration:
- Continue using Bootstrap: Ensure there is documentation and guidelines with style guides, coding standards, existing classes, and code examples. However, the codebase will continue to grow with new features and functionality
- Compare alternative frameworks: Research different frameworks, including Bulma, Foundation, and more. Identify better candidates to be used in the Umbraco backoffice. The new framework will most likely share the same problems as using Bootstrap
- Create a bespoke CSS framework: Create custom CSS rules and coding standards to tailor the solution specifically for Umbraco. Building a custom framework could be time consuming, especially upfront, with documentation a vital requirement for release. The codebase will continue to grow with new features and functionality
- Use a utility CSS library: Nothing is imposed with no predefined components, restrictions, or design decisions. A utility CSS library can be setup like a framework and can remain smaller in size, even as new features are added. But it will require a different way of thinking when making CSS changes
Regardless of which option is used for Umbraco, documentation must be created to give contributors defined guidance, rules, standards, and examples for making CSS changes.
Switching to a utility CSS library
There are lots of different utility CSS libraries and frameworks available. One of the most popular libraries is Tailwind CSS, a utility-first CSS framework that generates utility classes that "can be composed to build any design, directly in your markup".
Tailwind CSS is a utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup...
I first heard about Tailwind from Matt Brailsford during a CODECABIN event a few years ago. Ever since then, I have been an advocate for Tailwind, using the framework on multiple production websites and web apps at Rock Solid Knowledge. I have even seen previously sceptical colleagues adopt Tailwind for their own projects, as they have realised the benefits of having utility classes readily available to them.
Let's look at using Tailwind, answering some of the most common concerns, reservations, and questions that I get asked about using utility classes.
What are utility classes?
Tailwind uses your configuration setup to generate many classes that are named based on their (usually) single style. These classes can then be applied directly to your HTML. Most CSS frameworks, including Bootstrap, already use some utility classes for borders, colours, spacing, and more. For example, Bootstrap and Tailwind both have margin-bottom classes in the format mb-{x}:
.mb-4 {
margin-bottom: 1rem;
}
Do I have to learn another framework?
Bootstrap, like many other CSS frameworks, is an opinionated CSS framework. It provides a large library of predefined components regardless of whether you need them or not, including:
- Alerts
- Breadcrumbs
- Buttons
- Cards
- Forms
- Modals
Tailwind is an unopinionated CSS framework and does not generate any classes for components. Instead, it provides you with classes to add to your HTML to create your own components. In some regards, Tailwind can be considered a library of classes rather than a typical framework of components.
You don't have to learn a framework, but you will learn patterns for Tailwind class names, as found in the excellent Tailwind documentation. Class names are usually a single letter (or short, descriptive word), followed by a configurable value that mimics the output class style(s):
.mb-6 {
margin-bottom: 1.5rem;
}
.-mt-4 {
margin-top: -1rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.text-red {
color: red;
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.w-1/2 {
width: 50%;
}
.max-w-sm {
max-width: 24rem;
}
.z-10 {
z-index: 10;
}
Isn't it just inline styles?
There is a common misconception that using Tailwind is the same as using inline styles. While you are applying styles directly to your HTML elements, you are not creating your own rules and styles.
Tailwind also has many other advantages over inline styles:
- Constrained styles: Inline styles allow any valid CSS styles and values. Tailwind is constrained to a set of predefined classes generated by your configuration setup. Restricted styles make it easier to build and control consistent UIs, removing the ability to introduce new rules and styles
- Responsive styles: Inline styles can't use media queries. Tailwind classes can be used responsively to react to different screen sizes, media types (e.g. print), and even dark mode
- Hover, focus, and other stateful styles: Inline styles can't be used to style states like :hover or :focus. Tailwind uses state variant styles to change styles based on state
The example below shows how the background-color can change on the :hover state, and how the padding can change on the responsive state:
<p class="bg-white p-2 hover:bg-gray md:p-4">...</p>
It looks ugly and unreadable
At first glance, Tailwind can seem intense and I have heard it called "class soup" (due to the number of classes added to HTML elements). But once you are aware of the Tailwind syntax, the class names become intuitive, very readable, and explicitly define what styles are being added to a HTML element.
Below is an example of using semantic or generic class names to style a button component:
<button class="btn btn-blue">...</button>
.btn {
padding: 0.75rem 1rem;
}
.btn-blue {
background-color: blue; color: white;
}
And here is an example of using Tailwind class names to style the same button component:
<button class="bg-blue text-white px-4 py-3">...</button>
Both examples are applying the same styles, but you would only need to open the HTML file to make changes to the Tailwind version of the button component.
How can I peer review all those classes?
One major benefit of using utility classes is the readability of a pull request (PR). In a typical PR, an assigned peer reviewer of design changes would need to do one (or many) of the following:
- Checkout the repository code changes to review locally
- Review the code changes on a shared screen
- Review screenshots (or animated GIFs) attached to the PR
Umbraco recommends adding screenshots to PRs "whenever possible" to assist with the design review process. The example below shows how difficult it is to visualise the design change impact the card class has on the link-list:
<div class="card">...</div>
<ul class="link-list">...</ul>
.card {
margin-bottom: 0; /* was 2rem; */
}
It is very unlikely that the styles for the link-list will be visible in the PR, so it is not clear if removing margin-bottom from the card has any impact on link-list.
With Tailwind, the reviewer will be able to read that removing the margin class will not impact on the list spacing below:
<div class="mb-0">...</div>
<ul class="mt-4">...</ul>
Tailwind improves the PR process by adding an extra failsafe review point, reducing the risk of broken designs and layouts once approved.
Aren't all those classes bad for performance?
File compression, such as gzip and Brotli, can be used when transferring files between web servers and browsers. gzip replaces repeating strings with pointers to the first instance of that string, using a lot less space. More simply: the more repetition that exists in a file, the smaller the file size will be.
HTML files contain lots of repeating elements (such as div, p, and span), making them ideal gzip compression candidates. And because Tailwind class names also involve a lot of repetition, the compression algorithm works incredibly effectively on those class names too:
<p class="mb-4 text-red text-lg">...</p>
<ul class="mb-4 text-red text-lg">...</ul>
In comparison, the following semantic or generic CSS class names are unlikely to be compressed:
<p class="greeting">...</p>
<ul class="user-list">...</ul>
It is also best practice to remove unused CSS using a tool like PurgeCSS as part of the development workflow. Removing unused styles keeps file sizes as small as possible before they are minified by the existing ClientDependency Framework in Umbraco. Tailwind optimises CSS for production to maximise performance with built in PurgeCSS options that can be managed in your configuration setup.
It will take ages to swap out Bootstrap
It could take a while to swap Bootstrap for any CSS framework. There might be several new features and hundreds of bug fixes in Umbraco between starting the swap and releasing the finished product.
In 2018, Simon Vrachliotis demonstrated how he rebuilt an entire website using utility classes during a Christmas holiday break. He and I both agree that utility classes are "ridiculously fast" and use simpler workflows, in comparison to traditional methods.
Tailwind can help with an option to configure a custom prefix for all generated utility classes. This is useful in the interim of having Tailwind alongside Bootstrap, where there might be naming conflicts between the two frameworks.
Won't it be hard to maintain?
There is general concern that Tailwind classes would be hard to maintain. Managing many repeated classes and combinations could quickly become a large task in an application like Umbraco. There are two suggested solutions for maintaining utility classes and reducing duplication:
For smaller components, like form elements and buttons, Tailwind provides an @apply directive to extract component classes for common utility patterns:
<button class="btn btn-red">...</button>
.btn {
@apply block text-center px-4 py-3;
}
.btn-red {
@apply bg-red text-white;
}
Tailwind automatically replaces @apply statements with the exact styles as specified in the configuration setup.
In general, and to reduce larger component repetition, it is not recommended to extract component classes, as this is equivalent to writing custom CSS on top of Tailwind. Instead, it is better to extract template components using reusable partials and JavaScript components:
<promo
title="..."
img="..."
imgAlt="...">
</promo>
<template>
<article class="bg-white shadow mb-4 p-4">
<h2 class="text-blue text-lg mb-2">{{title}}</h2>
<img class="block border-2 border-gray" src="{{img}}" alt="{{imgAlt}}" />
</article>
</template>
In Umbraco this is ideal. AngularJS (and other JavaScript frameworks) use components to extract repetitive HTML, meaning class names are also extracted.
Will it go out of date?
If the CSS specification is updated with new features, such as CSS grid, it would be difficult for Bootstrap to add or convert components to use the new styles. It might even be a much later version of the framework that has the new capabilities.
Tailwind regularly releases utility classes in incremental upgrades. Because these utility classes are in addition to existing classes, they are not breaking changes. The new classes are available immediately after upgrade. And you don't have to use any new classes at all if you don't need them!
But if it is not possible to upgrade Tailwind for any reason, you can still create your own utility classes. For example, you may want to add filter grayscale styling. You can create your own utility classes that can be included with Tailwind:
@responsive {
.filter-none {
filter: none;
}
.filter-grayscale {
filter: grayscale(100%);
}
}
By using the @responsive code block, Tailwind also generates responsive variants for the filter classes:
<p class="filter-grayscale md:filter-none">...</p>
But I like writing CSS!
You are still writing CSS… in theory.
Unlike Bootstrap, where component styles are pre-generated, Tailwind makes you think in the same way as writing traditional CSS.
Think of Tailwind classes as shorthand for CSS styles and rules. If you wanted to create a white container with a shadow and equal padding in traditional CSS, you must understand what CSS values are needed to achieve the design:
.promo-block {
background: white;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
padding: 1rem;
}
In Tailwind, you solve the problem with the same approach. You must first understand what CSS values are needed, then apply the styles using utility classes instead of writing CSS:
<article class="bg-white shadow p-4">...</article>
In summary
Bootstrap has served Umbraco well. But lack of documentation, inconsistent standards, and other problems discussed have led to a growing and unmaintainable CSS codebase.
Utility CSS is a different approach to UI development. It is different to traditional methods. Yet it is important not to discount it straight away. When I was first introduced to Tailwind, I was also sceptical. After setting some time aside to try Tailwind on a new website, I fell in love with the speed and simpler workflow that it delivers. And it continues to deliver many months later in production too!
There will always be a need for CSS frameworks like Bootstrap. But as Umbraco transitions to .NET Core, and contributors begin to discuss the next viable backoffice JavaScript framework, it is now time to deliver a modern approach to CSS and UI development for Umbraco.
Please reach out to me, @karltynan on Twitter, if you'd like to chat about utility-first or Tailwind CSS.
Karl Tynan
Karl is on Twitter as @karltynan