As modern reactive front ends have begun to converge on effective module patterns and universal approaches to common application paradigms, it’s not uncommon to see much of the variance between front ends come from how they expose and implement their given style guide. The implementation and the delivery of application styles to an underlying component tree has become almost an art form, wherein each application attempts to solve the ever-present CSS problems of naming and reuse in the best, most-effective way possible. Tailwind CSS provides a set of style guide-agnostic utility classes backed by opinionated, customizable variables, allowing developers to switch between projects using the same CSS tool set.
Styling of the past
Stepping into a new frontend codebase involves several key learning curves that developers must understand and overcome in order to be successful. These proprietary, per-project concepts that must be relearned for each new application include common patterns such as component naming conventions, organization, and API interaction, with the steepest learning curve often being the underlying component CSS architecture. An application’s CSS architecture is usually deceivingly complex, consisting of naming conventions, color palettes, measurement scales, shared variables, utility classes, accessibility classes, and breakpoints, and developers must learn how each application uniquely approaches each of these concepts.
Many strategies are used in an attempt to overcome and to unify styling between projects in an effort to reduce developer time spent on styling. For example, CSS variables can be shared between components and their stylesheets, allowing for the reuse of colors, measurements, and utilities. Components can also be used to abstract away and to share typography, layout, and responsive styles in a declarative manner without the need for CSS at all. While these approaches can be effectively utilized to share most aspects of a given design system throughout a component tree, they only provide a mechanism for reuse and don’t actually lessen any learning curve for developers: variable names and available utility classes must still be learned, style components must be understood, and naming conventions and measurement scales must be memorized.
Another approach used to reduce the time developers spend on styling in a project is relying on a trusted third-party design framework. Many such frameworks exist that expose robust component sets and underlying CSS that prescribes a given look or feel that can be reused between projects (e.g. Material). Many of these off-the-shelf solutions work well and look great in practice, and they objectively solve the issue of developers needing to learn new styling approaches between projects, but they do so by handing developers a ready-made style guide rather than allowing a design team to create their own. It’s not uncommon for enterprise products to have underlying design systems that compete with the concepts provided by a third-party CSS solution, reducing the real-world usefulness and customizability of such design frameworks.
The future: Tailwind CSS
The idea of sharing variables for all major styling concepts throughout a codebase is inherently useful, and most projects already do this using hand-rolled naming conventions and value scales. It’s not uncommon to see
--color-green style color variables, or
.capitalize style typography classes, or
.flex style utility classes, or
--grid-base style measurement scales. Tailwind CSS provides a common set of CSS variables and utility classes so that projects no longer have to implement these in a proprietary manner.
The effectiveness of Tailwind can be thought of in two distinct categories: utility classes and base values. The utility classes exposed by Tailwind are intentionally low-level and can be used to apply any defined CSS rule to a given node in a declarative manner. For example, overflow can be hidden with
.overflow-x-hidden, text truncation can be added using
.truncate, and opacity can be cut by half using
.opacity-50. Almost every defined CSS rule has a corresponding utility class, and all utility classes are named using easy-to-remember conventions that are indicative of the underlying rule itself. At first, it may seem like this is just sugar over inline styles within components, but Tailwind goes further by also providing state-based variations of all defined utility classes so that styles can be conditionally-applied based on breakpoint, dark mode, and hover or focus states. For example, a black background can be conditionally-applied based on system dark mode simply by applying the
.dark:bg-black class. In this way, Tailwind mitigates the need for most custom CSS, and instead allows styling to be declarative using powerful classnames that don't change between projects.
The utility classes provided by Tailwind gain much of their power over raw inline styles because they rely on common internal values for things like measurements, colors, and breakpoints. This means that developers are not freely using any arbitrary values when applying Tailwind utility classes like they would be using inline styling, and they are instead restricted to what Tailwind defines. For example, the width of an object can be changed using classes such as
.w-1, .w-2, .w-3, etc., and Tailwind uses a carefully-chosen internal scale to determine what "width 1", "width 2" and "width 3" actually mean in terms of actual
width values. Tailwind opaquely provides base values like this for all utility rules that involve variables, most commonly measurements and colors. This means that developers can add medium vertical padding using
.pv-2, and increase this vertical padding on large screens with
.lg:pv-4, without ever caring about what the large breakpoint value is or what "padding 4" means. In this way, developers use Tailwind to apply any CSS rule, but these rules are backed by a subtle design system of values that cohesively work together no matter what is being designed.
Tailwind has broken new ground as a low-level, declarative CSS framework that’s also backed by a design system that provides just enough opinion to make developers effective. Perhaps most importantly, it does so in a manner that can be used repeatedly across different projects with different architectures and different application style guides.
The idea of a low-level utility CSS framework admittedly does not seem useful to many developers who have a firm grasp on CSS itself. Let’s look at an example to see how such utility classes provide objective advantages over hand-rolled styling by recreating the current Google search page.
First, let’s look at a vanilla CSS implementation of the current Google search page.
The example above has the advantage of being concise when only looking at the HTML because class names can be semantic and specific to the case in which they’re used. However, the example above also suffers from several key disadvantages that inhibit developers quickly stepping in and making modifications.
- Class names are arbitrarily named
While semantic class names are usually easy to follow, forcing developers to add custom CSS class names to nodes inherently opens a project up to subjective naming deviations and steepens the resulting learning curve. Different authors may choose different class names to accomplish similar styling, so the CSS must always be consulted for truth.
- The required CSS is verbose.
The resulting external CSS file is over 100 lines even with some reuse and streamlining of styling logic. While this could be made more concise if iterated on a few more times, this is inherently tedious to have to write so much custom CSS to accomplish styling that’s almost exclusively layout-based. In a component-based framework like React, this also requires an external CSS file to be created and imported, which is both tedious and incurs extra (albeit minimal) build time indirection.
- All CSS values used are arbitrary.
The resulting CSS also includes raw measurements for things like paddings, margins, font sizes, and colors. Leaving such values up the developer to choose without any restriction on those choices again opens styling up to subjective deviation. This could be mitigated by sharing CSS variables, but then those variable names also must be learned, which can change between projects.
Next, let’s look at a Tailwind CSS implementation of the current Google search page.
The example above has the disadvantage of having more-verbose markup due to more class names, as much of the CSS logic from the previous example has been moved to declarative class names in this case. However, developers also gain several key advantages that usually outweigh this added verbosity.
- All presentation logic is in a single location.
All markup and its corresponding styling is in a single, declarative location, allowing for easy mental absorption and easy declarative modification based on component state.
- Naming subjectivity is removed.
Since no custom CSS is needed, no hard-to-follow custom class names or supporting CSS variables or utility classes are needed. This greatly reduces the learning curve when switching between potentially-unrelated projects since the syntax and available CSS class set will be the same.
- All values used adhere to the Tailwind internal system.
When using styling that relies on values such as widths or colors, no longer are developers asked to loosely-adhere to an arbitrary measurement scale or to use proprietary color palette variables. Instead, Tailwind projects use the same utility classes, and the underlying values are automatically applied.
- Concepts are not per-project.
Perhaps most importantly, all concepts provided by Tailwind can be applied across projects. Because Tailwind straddles a unique line in which it’s both low level and also provides smart values, it can be effectively used in entirely unrelated projects with entirely different higher level style guides, with no additional ramp up at all.
Much of the power of Tailwind comes from its ability to pave over differences between projects and how they implement their utility CSS architecture. By providing a base design system that also includes uniform values for things like measurements, colors, and breakpoints, it may seem like Tailwind projects would always look and feel at least minimally similar. Luckily, Tailwind supports a robust customization model that allows different projects to customize key aspects of the system in a project-specific way, while still providing the exact same CSS class set to developers. This is an extremely powerful concept: through the use of a configuration file, it’s possible to customize key aspects of Tailwind’s default styling — such as a color palette — without changing any resulting syntax for developers. This level of iteratively-configurable customization — together with Tailwind being inherently low-level and sitting “beneath” the level of a full style guide — means that Tailwind applications do not look and feel the same in the way that Material or Bootstrap applications did in the past. Tailwind is a system to uniformly apply CSS, not a system of prefabricated components.
The implementation and the delivery of application styles is often given less thought than the implementation and delivery of an application’s component set. As a result, modern front ends have historically used proprietary, one-off solutions when implementing a style guide in code, requiring developers to learn new conventions when ramping up on a new project. Tailwind CSS attempts to provide a level of CSS utility classes that sits above raw CSS rules, streamlining developer productivity, while staying low-level enough to serve as a base for any styling flavor.