Utility-first CSS with Tailwind

Utility-first CSS is the notion of composing many small utilitarian classes together. With this, the aim is to allow you to create robust, scalable and responsive user interfaces for the web.

Tailwind is a CSS framework that provides a suite of utility classes out of the box. It also allows you to compose and add your own classes where required.

Tailwind doesn’t prescribe any specific look or feel. You’re free to build to your desired design without having to undo anything the framework offers up front.

A customisable config file drives the look and feel of your UI and provides default rules that you can tweak to suit your needs.

In this article, we’ll go into more depth on how you can use Tailwind to build a feature-rich user interface. We’ll also touch upon some of the more advanced features Tailwind has to offer.

Setup

Although Tailwind is available to use directly from a CDN it’s recommended to install it from npm. This allows you to take advantage of the full customisability Tailwind provides.

First, install Tailwind as a development dependency;

npm install tailwindcss --save-dev

Once installed you need to initialise Tailwind’s configuration setup;

./node_modules/.bin/tailwind init [custom filename]

This command generates a config file called tailwind.js (or the custom filename provided). This file contains all the predefined rules for colours, breakpoints, fonts, margins and more. The idea is that you remove a lot of the boilerplate and leave only the config required to build your UI. The file contains comments with an overview of each rule.

For example, the default responsive breakpoints provided by Tailwind are;

screens: {
  'sm':  '576px',
  'md':  '768px',
  'lg':  '992px',
  'xl':  '1200px',
},

These are the minimum widths used to generate CSS media queries.

If desired you could adjust these to be ergonomic breakpoints and even add your own;

screens: {
  'wrist':  '576px',
  'palm':  '768px',
  'lap':  '992px',
  'desk':  '1200px',
  'wall': '2560px'
},

Tailwind uses PostCSS to generate CSS output from the directives and config provided. This means integrating a build step into your current development process is necessary. Integration is possible via various build tools such as Webpack, Gulp and more. Being built on PostCSS means you’re able to leverage the ecosystem of plugins to adapt to your build process.

The simplest solution to get up and running is to create a CSS file and use the @tailwind directive to include all Tailwind’s utilities.

/* provides a set of base styles */
@tailwind preflight;

/* injects in any component classes created via plugins, place custom component classes below this */
@tailwind components;

/* injects in all Tailwind's utility classes, place custom utility classes below this */
@tailwind utilities;

You can use the Tailwind npm module to build the CSS;

./node_modules/.bin/tailwind build [input file path] -o [output file path]

You can then reference this output file path in a <link /> tag in your HTML;

<link href="/bundle.css" rel="stylesheet" />

Utility Classes

Tailwind provides utility classes for a wide array of needs. This allows you to build a responsive, bespoke UI without necessarily needing to write any new CSS.

For example .text-lg by default is 1.5 rem of the base font size. .bg-dark-red will apply a dark red background based on the settings in your config file.

The values set in rules are configurable from your config file. This means if you want .text-lg to be twice the size of your base font you can customise it to be.

You can apply state variants such as hover or focus in the same way as responsive styles. .hover:bg-blue will turn the background blue when hovering over the desired element.

To combine both responsive and state variants at the same time make sure you prefix the responsive variant first. .xl:hover:bg-blue will mean the background is only blue on hover at the xl breakpoint.

Anatomy of a Tailwind class
Anatomy of a Tailwind class

Below is an example of a card component with dark and light variations adapted from an example in Tailwind’s documentation.

This markup uses no custom CSS, just utility classes from Tailwind;

<div class="flex flex-wrap">
  <div class="p-6 w-full lg:w-1/2 xl:w-1/2">
    <div class="overflow-hidden shadow-lg">
      <img
        class="w-full"
        src="/sunset-warm.jpg"
        alt="Sunset with grass in the foreground"
      />
      <div class="px-6 py-4">
        <div class="font-bold text-xl mb-2">A Warm Sunset</div>
        <p class="text-grey-darker text-base">
          A large drop of sun lingered on the horizon and then dripped over and
          was gone, and the sky was brilliant over the spot where it had gone,
          and a torn cloud, like a bloody rag, hung over the spot of its going.
          And dusk crept over the sky from the eastern horizon, and darkness
          crept over the land from the east.
        </p>
      </div>
      <div class="px-6 py-4">
        <span
          class="inline-block bg-grey-lighter rounded-full px-3 py-1 text-sm font-semibold text-grey-darker cursor-pointer hover:text-grey-lighter hover:bg-grey-darker mr-2"
          >#photography</span
        >
        <span
          class="inline-block bg-grey-lighter rounded-full px-3 py-1 text-sm font-semibold text-grey-darker cursor-pointer hover:text-grey-lighter hover:bg-grey-darker mr-2"
          >#sunset</span
        >
        <span
          class="inline-block bg-grey-lighter rounded-full px-3 py-1 text-sm font-semibold text-grey-darker cursor-pointer hover:text-grey-lighter hover:bg-grey-darker"
          >#summer</span
        >
      </div>
    </div>
  </div>

  <div class="p-6 w-full lg:w-1/2 xl:w-1/2">
    <div class="overflow-hidden shadow-lg bg-grey-darkest">
      <img
        class="w-full"
        src="/sunset-cold.jpg"
        alt="Sunset with grass in the foreground"
      />
      <div class="px-6 py-4">
        <div class="font-bold text-xl mb-2 text-grey-light">A Cold Sunset</div>
        <p class="text-grey text-base">
          A large drop of sun lingered on the horizon and then dripped over and
          was gone, and the sky was brilliant over the spot where it had gone,
          and a torn cloud, like a bloody rag, hung over the spot of its going.
          And dusk crept over the sky from the eastern horizon, and darkness
          crept over the land from the east.
        </p>
      </div>
      <div class="px-6 py-4">
        <span
          class="inline-block bg-grey-darker rounded-full px-3 py-1 text-sm font-semibold text-grey-lighter cursor-pointer hover:text-grey-darker hover:bg-grey-lighter mr-2"
          >#photography</span
        >
        <span
          class="inline-block bg-grey-darker rounded-full px-3 py-1 text-sm font-semibold text-grey-lighter cursor-pointer hover:text-grey-darker hover:bg-grey-lighter mr-2"
          >#sunset</span
        >
        <span
          class="inline-block bg-grey-darker rounded-full px-3 py-1 text-sm font-semibold text-grey-lighter cursor-pointer hover:text-grey-darker hover:bg-grey-lighter"
          >#winter</span
        >
      </div>
    </div>
  </div>
</div>

View example on CodeSandbox.

One of the most powerful features we’re using here is the ability to create responsive grids with just a few classes. Flexbox powers this functionality under the hood.

By applying an outer .flex class we’re able to add classes to inner elements which dictate their widths. For example, w-full lg:w-1/2 xl:w-1/2 will create a full-width element at all breakpoints other than lg and xl, where the width will be half.

With this, you can create complex layouts by composing utility classes. You’re also free to create a grid layout that works for you rather than being prescribed one.

Generally, the utility class name syntax may take a while to get used to due to its shorthand nature. The comments within the generated config file and detailed website documentation are helpful.

Component Classes

As you may have noticed in the example above you can get to a point where you’re applying lots of utility classes to a single element. Use the same element in many places and you’re now having to update each one every time you need to make a tweak. This isn’t scalable.

Luckily Tailwind has a solution to reduce this in the form of component classes.

Component classes allow you to extract many utility classes as well as custom CSS into new classes. This will provide you with a single source of truth for a specific piece of functionality.

Using Tailwind’s @apply directive we can create component classes to abstract these rules.

Here’s the exact same design as the card example above built using component classes where deemed necessary;

<div class="flex flex-wrap">
  <div class="p-6 w-full lg:w-1/2 xl:w-1/2">
    <div class="card">
      <img
        class="w-full"
        src="/sunset-warm.jpg"
        alt="Sunset with grass in the foreground"
      />
      <div class="px-6 py-4">
        <div class="heading">A Warm Sunset</div>
        <p class="text-grey-darker text-base">
          A large drop of sun lingered on the horizon and then dripped over and
          was gone, and the sky was brilliant over the spot where it had gone,
          and a torn cloud, like a bloody rag, hung over the spot of its going.
          And dusk crept over the sky from the eastern horizon, and darkness
          crept over the land from the east.
        </p>
      </div>
      <div class="px-6 py-4">
        <span class="pill pill-light hover:pill-light mr-2">#photography</span>
        <span class="pill pill-light hover:pill-light mr-2">#sunset</span>
        <span class="pill pill-light hover:pill-light">#summer</span>
      </div>
    </div>
  </div>

  <div class="p-6 w-full lg:w-1/2 xl:w-1/2">
    <div class="card bg-grey-darkest">
      <img
        class="w-full"
        src="/sunset-cold.jpg"
        alt="Sunset with grass in the foreground"
      />
      <div class="px-6 py-4 ">
        <div class="heading text-grey-light">A Cold Sunset</div>
        <p class="text-grey text-base">
          A large drop of sun lingered on the horizon and then dripped over and
          was gone, and the sky was brilliant over the spot where it had gone,
          and a torn cloud, like a bloody rag, hung over the spot of its going.
          And dusk crept over the sky from the eastern horizon, and darkness
          crept over the land from the east.
        </p>
      </div>
      <div class="px-6 py-4">
        <span class="pill pill-dark hover:pill-dark mr-2">#photography</span>
        <span class="pill pill-dark hover:pill-dark mr-2">#sunset</span>
        <span class="pill pill-dark hover:pill-dark">#winter</span>
      </div>
    </div>
  </div>
</div>

And here’s the newly created classes;

.heading {
  @apply font-bold text-xl mb-2;
}

.card {
  @apply overflow-hidden shadow-lg;
}

.pill {
  @apply inline-block rounded-full px-3 py-1 text-sm font-semibold mr-2 cursor-pointer;
}

@variants hover {
  .pill-light {
    @apply text-grey-lighter bg-grey-darker;
  }
  .pill-dark {
    @apply text-grey-darker bg-grey-lighter;
  }
}

.pill-light {
  @apply bg-grey-lighter text-grey-darker;
}

.pill-dark {
  @apply bg-grey-darker text-grey-lighter;
}

This approach reduces the number of classes in the markup whilst delivering the exact same design. We’ve moved reused patterns to new classes allowing a single source of truth.

You’ll also see that we’re still composing utility classes alongside component classes. It’s not a choice between one or the other meaning you can have the best of both worlds.

For example, there’s no need to create a .card-dark class as that’d end up being @apply bg-grey-darkest;. You can use the .bg-grey-darkest class alongside the .card class in the markup.

This is one of the core concepts that gives Tailwind its power. While utilitarian at first glance Tailwind doesn’t force that pattern upon you.

Directives

Tailwind comes with a suite of predefined directives, we’ve come across three in this article already;

  • the @tailwind directive which allows you to import the core of Tailwind
  • the @apply directive which allows you to create new classes by composing other classes
  • and the @variants directive, which we’ll take a deeper look at now

In the card example above we’re using the @variant directive to merge our pill hover styles into a single class. This is a powerful way of creating stateful variants of your own utility or component classes. You can use the @variant directive to create focus, active or group-hover specific classes.

Here’s how we used the span style="font-weight: 400;">@variants rule in the example above to create hover variants for our custom component classes;

@variants hover {
  .pill-light {
    @apply text-grey-lighter bg-grey-darker;
  }
  .pill-dark {
    @apply text-grey-darker bg-grey-lighter;
  }
}

The @responsive directive will create a suite of responsive classes for any new classes you wrap within it;

@responsive {
  .blur {
    filter: blur(30px);
  }
}


This will generate classes prefixed with your responsive breakpoint names. For example, .sm:blur will only apply a blur to images at the small breakpoint.

Finally, the @screen directive allows you to target the already defined breakpoints. This avoids rewriting media queries and having to keep them in sync.

@screen md {
  .blur {
    filter: blur(20px);
  }
}

@screen lg {
  .blur {
    filter: blur(10px);
  }
}

This example will provide stepped levels of blurriness depending on the breakpoint to any images with the blur class applied to it.

Directives are another powerful way to extend custom classes. They can help provide all the features you get from Tailwind’s utility classes to your own custom classes.

CSS-in-JS Integration

Here at NearForm, we use a variety of solutions to build user interfaces. We tailor solutions to the specific needs of clients and their projects.

CSS-in-JS libraries such as Styled Components or Emotion as well as frameworks such as Material-UI are amongst our arsenal. Given this, it’s worth investigating how Tailwind integrates with these tools.

Here’s an example of usage with Styled Components and React;

import React from "react";
import styled from "styled-components";
import tw from "tailwind.macro";
import sunsetWarm from "./sunset-warm.jpg";

const Container = styled.div`
  ${tw`p-6 w-full lg:w-1/2 xl:w-1/2`}
`;

const Card = styled.div`
  ${tw`overflow-hidden shadow-lg`}
`;

const Image = styled.img`
  ${tw`w-full`}
`;

const InnerContainer = styled.div`
  ${tw`px-6 py-4`}
`;

const Text = styled.div`
  ${props => props.type === "heading" && tw`font-bold text-xl mb-2`}
  ${props =>
    (props.type === "paragraph" || !props.type) &&
    tw`text-grey-darker text-base`}
`;

const Pill = styled.span`
  ${tw`inline-block bg-grey-lighter rounded-full px-3 py-1 text-sm font-semibold text-grey-darker cursor-pointer hover:text-grey-lighter hover:bg-grey-darker`}

  &:not(:last-child) {
    ${tw`mr-2`}
  }
`;

export const App = () => (
  <Container>
    <Card>
      <Image src={sunsetWarm} alt="Sunset with grass in the foreground" />
      <InnerContainer>
        <Text type="heading">A Warm Sunset</Text>
        <Text type="paragraph" as="p">
          A large drop of sun lingered on the horizon and then dripped over and
          was gone, and the sky was brilliant over the spot where it had gone,
          and a torn cloud, like a bloody rag, hung over the spot of its going.
          And dusk crept over the sky from the eastern horizon, and darkness
          crept over the land from the east.
        </Text>
      </InnerContainer>
      <InnerContainer>
        <Pill>#photography</Pill>
        <Pill>#sunset</Pill>
        <Pill>#summer</Pill>
      </InnerContainer>
    </Card>
  </Container>
);

In the example above we’re using the Babel Macro babel-plugin-tailwind-components. This plugin exposes the tw`` tagged template literal function to convert Tailwind classes into style objects.

This example works well and allows further abstraction via Styled Component’s visual primitives.

There are some caveats worth highlighting when trying to integrate Tailwind into a CSS-in-JS toolchain;

  • You’re still going to need to amend your toolchain to accommodate a build step for Tailwind
  • You’ve now got two places you can write styles. How do you ensure consistency?
  • The component style of a library like React feels somewhat at odds with the component style of Tailwind. You’re now able to componentise either via Tailwind with the @apply directive or in React with a component. A decision is now required on where any abstractions live.

If CSS-in-JS is your jam then there are ways to integrate the two workflows. However, you may end up not gaining the full benefits of either solution when used together.

Production Builds

By default Tailwind clocks in at around 36kb in size without any customisation, once minified and gzipped.

Once you begin to configure Tailwind to suit your UI look and feel you’ll be able to remove a lot of the predefined configuration Tailwind provides.

For example, if you trim the colour configuration down from the 73 colours provided by Tailwind to 25 colours you’ll reduce the bundle size down to 18kb. Similarly, if you reduce down the breakpoints from 5 to 3 you’ll shave 14kb off the bundle size.

Finally, by integrating a tool like PurgeCSS into your workflow you can remove any unused classes at build time. This means you’re only ever serving the classes your application uses.

Final Thoughts

Tailwind takes the benefits of CSS utility classes and extends upon them. By allowing composability and extensibility Tailwind provides an impressive developer experience. One where you can be productive straight away whilst not being tied to a predefined look and feel.

The lack of prescribed visual styles can be a huge boost to productivity. The speed with which you can compose a complex, responsive UI with its own look and feel is staggering.

If you looking for a team to deliver a rapid prototype using tools like Tailwind then don’t hesitate to get in touch.

? Cover image credit 

Top