Frontend & Hyvä 1/21/2021

Maintainability in Utility-First Frameworks in the example of Tailwind CSS

The new Magento 2 Frontend Hyvä uses Tailwind CSS, the most popular utility-first framework, for styling. While working with Tailwind CSS, having some general rules of thumb helps a lot with clarity, reusability and readbility of code.

Utility-first frameworks like Tailwind CSS provide a comprehensive set of utility classes. One single utility class translates a css property:value pair into a reusable class, that is usually given a short acronym, e. g.:

<h1 class="pb-4"></h1>

is equivalent to

<h1 class="title"></h1>
.title {
    padding-bottom: 1rem;
}

One substantial strength of the utility approach is the reduction of arbitrary abstraction. The user is not obliged to assign appropriate names to class or id selectors. A task that can be assumed to show a high grade of uncertainty.


What does it mean, uncertainty? The personal process of assigning names to selectors most presumably ends up in low repeatability and even lower reproducibility. One might find himself/herself in a situation, where he/she realizes to have chosen multiple patterns to build e. g. class names in a single project. A third person will do, with some probability, things totally different, adding additional patterns to the amount of chosen approaches.


This situation is a source of inconsistency that in consequence offers downstream problems like e. g. redundancy, increased error probability and rising debugging or adaptation efforts.

Utility-first frameworks provide a solution to solve this problem to a broad extend. Tailwind CSS describes this as follows:

Tailwind encourages a utility-first workflow, where designs are initially implemented using only utility classes to avoid premature abstraction.
Extracting Components - Dealing with duplication and keeping utility-first projects maintainable.

In some way this is a deal with the devil. The approach manages to reduce abstraction significantly. But like all concepts in the history of CSS the benefits gained come at a price. The following essay tries to identify working approaches that support creating maintainable results in applying a utility-first approach. Here, maintainability describes the situation that a third person is able to find patterns easily to become familiar with existing templates they deal with at first time.

The examples shown rely on templates that use Tailwind CSS.

Sort utility classes

A missing pattern in sorting utility classes increases the risk of redundant application. Getting in touch with an arbitrarily sorted list of classes makes it difficult to understand the logic applied. The attempt to identify a particular class becomes more difficult and consumes attention and time, unnecessarily.

The advise is to use an automated process to sort utility classes by the means of a defined pattern. This enforces consistency across an entire team. It facilitates taking over a project from team members or getting into a new one. The selection below applies a configurable regex. They are quickly installed/removed and worth a try.

Headwind

Users of Visual Studio Code might find benefit in installing Headwind. It is installed in no time and provides a clear set of configurable settings.

Prettier Plugin Tailwind

If prettier is a favored option, it arguably makes sense to integrate a sorting automatism by installing a prettier plugin like Prettier Tailwind Plugin or Prettier Plugin Tailwind.

Iron Discipline

If no automated tool is available, one can only rely on iron discipline. A great help is to group classes after their property in ascending order starting with the non-prefixed variant.

Bad: no particular sort order

<div class="sm:text-3xl text-2xl font-medium sm:mb-8 mb-4 text-gray-900 lg:text-4xl"></div>

Good: screen resolution specific adaptations in ascending sort order

<div class="font-medium text-gray-900 mb-4 md:mb-8 text-2xl sm:text-3xl lg:text-4xl"></div>

Regex based tools allow appending or prepending classes that apply an entire set of utility classes, Tailwind CSS calls them template components. They are hard to find in a list of utility classes. The isolation of template components in a class attribute benefit work efficiency. If no automated sorting tool is applied, other approaches like bracketing (i. e. [ ]) may also find supporters to highlight non-utility classes. Bracketing is applied by Cube CSS in a different context.

Bad: arbitrary positioning of template class .product-tile

<div class="sm:text-3xl text-2xl font-medium product-tile mb-4 text-gray-900"></div>

Good: rule based positioning of template classes by prepending, appending or bracketing of .product-tile

<div class="product-tile font-medium mb-4 sm:mb-8 text-2xl sm:text-3xl text-gray-900"></div>
<div class="font-medium mb-4 sm:mb-8 text-2xl sm:text-3xl text-gray-900 product-tile"></div>
<div class="[ product-tile ] font-medium mb-4 sm:mb-8 text-2xl sm:text-3xl text-gray-900"></div>

Start with applying styles on mobile screens

Higher screen width often requires additional style definitions to display content that is properly readable in terms of human perception. Smaller screen do not suffer from this. They are often appealing without further ado. Starting with applying styles on higher screen width often break things that worked perfectly on small screens. In consequence additional styles are applied in order to reconstruct native browser behavior on small screens. This inflates the number of style statements applied. The advise is to start on mobile screens, if not disproved argumentatively.

Tailwind CSS encourages this approach, due to the way breakpoints are defined in the default template:

// tailwind.config.js
module.exports = {
  theme: {
    screens: {
      'sm': '640px',
      // => @media (min-width: 640px) { ... }

      'md': '768px',
      // => @media (min-width: 768px) { ... }

      'lg': '1024px',
      // => @media (min-width: 1024px) { ... }

      'xl': '1280px',
      // => @media (min-width: 1280px) { ... }

      '2xl': '1536px',
      // => @media (min-width: 1536px) { ... }
    }
  }
}

Breakpoints - Customizing the default breakpoints for your project

Building small screen width first has two major benefits:

  • the creation process is in line with default media breakpoints defined by tailwind
  • on average less statements are applied

Use one utility class per @apply statement

Concatenating utility classes may appear convenient. However, it hardens the life of those who follow. There are two major benefits of single @apply statements:
  • Faster navigation and edit with keyboard shortcuts in IDE
  • Use the power of auto code formatting in IDE
  • Clearer display of changes between commits (e. g. in Git)
Bad: multiple utility classes per @apply statement
.actions-toolbar {
  @apply mt-6 border-t border-background pt-4;
}

Good: one utility class per @apply statement

.actions-toolbar {
  @apply mt-6;
  @apply border-t;
  @apply border-background;
  @apply pt-4;
}

Deal with stylistically insignificant classes on a common basis

The example shows classes action and submit that are relicts of a template transfer into a tailwind setup. They apply no styles. Utility classes were added after the template transfer. One might be tempted to keep classes for descriptive purposes or add new ones for orientation purposes or as an addressable selector that might by useful in indeterminate future scenarios. You might want to call them 'dead classes', 'marker classes', 'lands marks', 'descriptive classes' or any other term that might fit.

Option A: class listing contains classes that apply no declarations

<div class="xl:my-12 action submit text-primary primary my-6"></div>

Option B: class listing contains only stylistically significant classes

<div class="text-primary my-6 xl:my-12"></div>

The issue offers room for discussions. The appended list offers points of view that might help in finding a personal position.

  • In context of the Magento framework class names applied may give a hint towards the corresponding block name.

  • Marker classes allow selecting DOM elements. In case of urgency marker classes allow manipulating styles by adding layout updates in the admin panel without committing a formal change to the version control.

  • In compiler theory dead classes are subject to dead code elimination, since they have no relevance for executing the program. In a context of clean coding principles one can describe them dirty code.

  • The main purpose of attribute selectors like classes is to assign style decisions, not to convey meaning.

  • Clean Code suggests the principle of single responsibility. Applying this logic on the usage of the class selector reveals a conflict in presence of utility classes.

  • The usage of dead classes contradicts the approach of utility-first frameworks to avoid premature abstraction.

  • Missing orientation in template files is a testimony of insufficient content declaration, dead classes should not justify another insufficiency.

  • A sequence of utility classes behaves like a fingerprint and can be used to select specific DOM elements in case of urgency.

Use kebab case for CSS selectors

There is a set of case styles available. The decision on applying a certain case style depends on common practice, the context and sometimes personal taste. The important point is to agree on a common standard.

Bad: no uniform application of case styles

<div class="button_primary"></div>
<div id="telephoneNumber"></div>

Good: uniform application of kebab case style

<div class="button-primary"></div>
<div id="telephone-number"></div>

Don't rely on abbreviations to define components

The ability to distinguish utility classes from component classes at first sight is important e. g. to clarify specificity. Using abbreviations make component classes look similar to utility classes. This weakens readability. In contrast, the usage of acronyms that present recognized concepts in a human readable way is recommended, e. g. "cms" for content management system.

Bad: usage of colloquial abbreviations

<div class="btn btn-primary"></div>

Good: usage of spelled out terms

<div class="button button-primary"></div>

Make restrained usage of template components

Template components describe a set of utility classes that is extracted from the original template file and referenced by an arbitrarily selected class name.

<div class="button">...</div>
.button {
  @apply py-2;
  @apply px-1;
  @apply border;
  @apply border-solid;
  @apply border-blue-200;
}

In some circumstances the creation of a template component is the favorable approach. However, it should be chosen wisely and avoided as far as possible, since it contradicts the approach of refraining from premature abstraction. The extraction of utility classes has some major downsides.

  • It increases the size of the final CSS file and negatively affects page speed ranking.

  • Adapting template components requires recompilation of the final CSS file and slows down the development process

Nonetheless, at some point maintainability of a project is severely threatened by not using the option to extract a set of utility classes.

The key is to identify those elements of a page that are defined once, adapted rarely and used frequently. Often this pattern describes page elements like button, forms, tiles or boxes, etc.

Using sorted sequences of utility classes is very approachable by search and replace functionality of the IDE. Try to exploit this potential before creating template components.

Plugins like Tailwind Custom Forms substantially reduce the need for template components.

Separate concerns

Layout and Design

The clean code principles suggest the seperation of concerns. The idea is well applicable in the context of utility classes.

The layout of a webpage is hard to imagine, if the underlying HTML markup is not chatty about CSS declarations affecting the layout of the page. This is a major benefit of utility-first frameworks. They allow to convey this information directly in the template file.

This gets especially important, when thinking about carrying out an extraction of utility classes. Refrain from creating template classes that mix declaration affecting the layout and design. Leave information that communicates the layout in the template file.

There is one exception. Sometimes it makes sense to extract utility classes that decide upon the layout of a page. In this case create dedicated template components that only carry layout instructions. I call them layout components. One prominent example is the class container, that usually takes responsibility for giving shape to a web shop, when a full width display of content is not intended.

Bad: layout related declarations hidden in a template class

<div class="account-tile">
  <div class="col-span-6"></div>
  <div class="col-span-6"></div>
</div>
.account-tile {
  @apply grid;
  @apply grid-cols-12;
  @apply gap-2;
  @apply p-2;
  @apply border;
  @apply border-gray-200;
  @apply h-full;
  @apply my-1;
  @apply transition-shadow;
  @apply duration-150;
  @apply ease-linear;
}

Good: layout decisions presented in template

<div class="account-tile grid grid-cols-12 gap-2">
  <div class="col-span-6"></div>
  <div class="col-span-6"></div>
</div>
.account-tile {
  @apply p-2;
  @apply border;
  @apply border-gray-200;
  @apply h-full;
  @apply my-1;
  @apply transition-shadow;
  @apply duration-150;
  @apply ease-linear;
}

Screen variants

Another example of the necessity to separate concerns provides the case of a gallery navigation. The client may intend to display navigation dots on mobile devices and thumbnails on the rest of the device landscape. Throwing all those circumstances into the class attribute results in an unmaintainable pile of declarations.

Using template components to separate concerns gives clarity on where to act.

Bad: undifferentiated concatenation of utility classes

<a
  href="#"
  class="inline-block outline-none focus:outline-none rounded-full h-2 w-2 md:w-auto md:h-auto md:border-b-2 md:rounded-none md:border-transparent"
  :class="{ 'bg-primary md:bg-transparent md:border-primary' : active === index+1, 'bg-background md:bg-transparent' : active !== index+1}"
  @click.prevent="scrollLeft($refs.sliderProductMedia)"
>
  <img :src="image.thumb" />
</a>

Good: resolution specific style variants grouped in template classes

<a
  href="#"
  class="[ navigation-dot md:navigation-thumbnail ] inline-block outline-none focus:outline-none"
  :class="{ 'selected' : active === index+1, 'unselected' : active !== index+1}"
  @click.prevent="$refs.sliderProductMedia.scrollLeft = ($refs.sliderProductMedia.scrollWidth / images.length) * index; setActive(index+1);"
>
  <img :src="image.thumb" />
</a>
.navigation-dot {
  @apply h-2;
  @apply w-2;
  @apply rounded-full;
}

.navigation-thumbnail {
  @apply h-auto;
  @apply w-auto;
  @apply rounded-none;
  @apply border-b-2;
  @apply border-transparent;
}

.selected {
  @apply bg-primary;
}

.unselected {
  @apply bg-background;
}

@screen md {
  .selected {
    @apply bg-transparent;
    @apply border-primary;
  }

  .unselected {
    @apply bg-transparent;
  }
}

Personal conclusion

The basic concept of utility-first does not provide a solution for long-lasting fundamental challenges in the application of CSS. Utility-first is an additional way of approaching a task and getting a job done, efficiently and commercially profitable. Its strength lies in the recognition that the traditional way of summarizing multiple style declarations in an arbitrarily named class is an act of abstraction, that is characterized by a specific set of downstream challenges. If this step is taken out of the equation, a new set of challenges arises, while others vanish. It is important to develop your intuition with regards to the boundaries of utility-first. Playing out the strengths of the utility-first approach requires a basic understanding, at which point the application of traditional patterns and procedures is disadvantageous. The ideas mentioned in this contribution do not claim to be the ultimate sources of truth. Different conclusions of bad/good assignments can be argumentatively valid and beneficial depending on the context. The main goal is to make work results more approachable for oneself and others. The requirement to reach this goal is to find a common understanding of how to use a tool in a team. The previously mentioned ideas resulted from my first project with Tailwind CSS in the context of Hyvä.