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.
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.
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 . 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 or
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 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) { ... } } } }
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)
.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 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; } }