Extracting Components

Dealing with duplication and keeping utility-first projects maintainable.

Tailwind encourages a utility-first workflow, where designs are initially implemented using only utility classes to avoid premature abstraction.

Woman paying for a purchase
Marketing
Finding customers for your new business

Getting a new business off the ground is a lot of hard work. Here are five ideas you can use to find your first customers.

<!-- A marketing page card built entirely with utility classes -->
<div class="md:flex">
  <div class="md:flex-shrink-0">
    <img class="rounded-lg md:w-56" src="https://images.unsplash.com/photo-1556740738-b6a63e27c4df?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=448&q=80" alt="Woman paying for a purchase">
  </div>
  <div class="mt-4 md:mt-0 md:ml-6">
    <div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">Marketing</div>
    <a href="#" class="block mt-1 text-lg leading-tight font-semibold text-gray-900 hover:underline">Finding customers for your new business</a>
    <p class="mt-2 text-gray-600">Getting a new business off the ground is a lot of hard work. Here are five ideas you can use to find your first customers.</p>
  </div>
</div>

But as a project grows, you'll inevitably find yourself repeating common utility combinations to recreate the same component in many different places. This is most apparent with small components, like buttons, form elements, badges, etc.

<!-- Repeating these classes for every button can be painful -->
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Button
</button>

Keeping a long list of utility classes in sync across many component instances can quickly become a real maintenance burden, so when you start running into painful duplication like this, it's a good idea to extract a component.


Extracting HTML components

It's very rare that all of the information needed to define a UI component can live entirely in CSS — there's almost always some important corresponding HTML structure you need to use as well.

Don't rely on CSS classes to extract complex components

Beach
Private Villa
$299 USD per night
<style>
  .vacation-card { /* ... */ }
  .vacation-card-info { /* ... */ }
  .vacation-card-eyebrow { /* ... */ }
  .vacation-card-title { /* ... */ }
  .vacation-card-price { /* ... */ }
</style>

<!-- Even with custom CSS, you still need to duplicate this HTML structure -->
<div class="vacation-card">
  <img class="vacation-card-image" src="..." alt="Beach in Cancun">
  <div class="vacation-card-info">
    <div>
      <div class="vacation-card-eyebrow">Private Villa</div>
      <div class="vacation-card-title">
        <a href="/vacations/cancun">Relaxing All-Inclusive Resort in Cancun</a>
      </div>
      <div class="vacation-card-price">$299 USD per night</div>
    </div>
  </div>
</div>

For this reason, it's often better to extract reusable pieces of your UI into template partials or JavaScript components instead of writing custom CSS classes.

By creating a single source of truth for a template, you can keep using utility classes without any of the maintenance burden created by duplicating the same classes in multiple places.

Create a template partial or JavaScript component

<!-- In use -->
<VacationCard
  img="/img/cancun.jpg"
  imgAlt="Beach in Cancun"
  eyebrow="Private Villa"
  title="Relaxing All-Inclusive Resort in Cancun"
  pricing="$299 USD per night"
  url="/vacations/cancun"
/>

<!-- ./components/VacationCard.vue -->
<template>
  <div>
    <img class="rounded" :src="img" :alt="imgAlt">
    <div class="mt-2">
      <div>
        <div class="text-xs text-gray-600 uppercase font-bold">{{ eyebrow }}</div>
        <div class="font-bold text-gray-700 leading-snug">
          <a :href="url" class="hover:underline">{{ title }}</a>
        </div>
        <div class="mt-2 text-sm text-gray-600">{{ pricing }}</div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    props: ['img', 'imgAlt', 'eyebrow', 'title', 'pricing', 'url']
  }
</script>

The above example uses Vue, but the same approach can be used with React components, ERB partials, Blade components, Twig includes, etc.


Extracting CSS components with @apply

For small components like buttons and form elements, creating a template partial or JavaScript component can often feel too heavy compared to a simple CSS class.

In these situations, you can use Tailwind's @apply directive to easily extract common utility patterns to CSS component classes.

Here's what a .btn-blue class might look like using @apply to compose it from existing utilities:

<button class="btn-blue">
  Button
</button>

<style>
.btn-blue {
  @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.btn-blue:hover {
  @apply bg-blue-700;
}
</style>

Note that variants like hover:, focus:, and {screen}: can't be applied directly, so instead apply the plain version of the utility to the appropriate pseudo-selector or media query.

Classes like this should be added directly after the @tailwind components directive to avoid specificity issues.

Don't add custom component classes to the end of your CSS

@tailwind base;

@tailwind components;

@tailwind utilities;

.btn-blue {
  @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.btn-blue:hover {
  @apply bg-blue-700;
}

Add custom component classes before your utilities

@tailwind base;

@tailwind components;

.btn-blue {
  @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.btn-blue:hover {
  @apply bg-blue-700;
}

@tailwind utilities;

Keeping things composable

Say you have these two buttons:

<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Button
</button>

<button class="bg-gray-400 hover:bg-gray-500 text-gray-800 font-bold py-2 px-4 rounded">
  Button
</button>

It might be tempting to implement component classes for these buttons like this:

.btn-blue {
  @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.btn-blue:hover {
  @apply bg-blue-700;
}

.btn-gray {
  @apply bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded;
}
.btn-gray:hover {
  @apply bg-gray-500;
}

The issue with this approach is that you still have potentially painful duplication.

If you wanted to change the padding, font weight, or border radius of all the buttons on your site, you'd need to update every button class.

A better approach is to extract the parts that are the same into a separate class:

.btn {
  @apply font-bold py-2 px-4 rounded;
}

.btn-blue {
  @apply bg-blue-500 text-white;
}
.btn-blue:hover {
  @apply bg-blue-700;
}

.btn-gray {
  @apply bg-gray-400 text-gray-800;
}
.btn-gray:hover {
  @apply bg-gray-500;
}

Now you'd apply two classes any time you needed to style a button:

<button class="btn btn-blue">
  Button
</button>

<button class="btn btn-gray">
  Button
</button>

This makes it easy to change the shared styles in one place by just editing the .btn class.

It also allows you to add new one-off button styles without being forced to create a new component class or duplicate the shared styles:

<button class="btn bg-green-500 hover:bg-green-400 text-white">
  Button
</button>

Writing a component plugin

In addition to writing component classes directly in your CSS files, you can also add component classes to Tailwind by writing your own plugin:

// tailwind.config.js
module.exports = {
  plugins: [
    function({ addComponents }) {
      const buttons = {
        '.btn': {
          padding: '.5rem 1rem',
          borderRadius: '.25rem',
          fontWeight: '600',
        },
        '.btn-blue': {
          backgroundColor: '#3490dc',
          color: '#fff',
          '&:hover': {
            backgroundColor: '#2779bd'
          },
        },
        '.btn-red': {
          backgroundColor: '#e3342f',
          color: '#fff',
          '&:hover': {
            backgroundColor: '#cc1f1a'
          },
        },
      }

      addComponents(buttons)
    }
  ]
}

This can be a good choice if you want to publish your Tailwind components as a library or make it easier to share components across multiple projects.

Learn more in the component plugin documentation.