Tailwind CSS Intermediate: Theme, Variants and Reuse
Reusing Styles and Components
In this lesson you will
- Explain why components are the main way to reuse Tailwind styles
- Use @apply and @utility correctly and know their downsides
- Choose between a component, a utility and a CSS class
Introduction
A common first reaction to Tailwind is: "my HTML is full of long class lists, and I repeat them everywhere". That is a real concern, and it has a standard answer. This lesson explains the options and which to prefer.
First: is it really repetition?
If you repeat the same button markup ten times in static HTML, you have a duplication problem in the markup, not only in the classes. Fixing it means creating something reusable, whatever you use for styling.
Option 1: Components (preferred)
In a framework (React, Vue, Svelte, Astro, Angular) or a template language, put the markup and its classes into one component.
React (JSX)
function Button({ children }) {
return (
<button className="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700">
{children}
</button>
);
}Now the class list lives in exactly one place. Change it once, and every button updates. Templates, includes and partials work the same way in plain server-side rendering.
Components are the recommended way to reuse Tailwind styles, because they reuse structure, behaviour and appearance together.
Quick quiz
What is the main recommended way to avoid repeating Tailwind classes?
Option 2: Multi-cursor editing and loops
For repeated items in a single file, a loop (rendering a list from data) already avoids duplication, and editors let you change many identical class lists at once for quick edits.
Option 3: @apply for CSS classes
@apply pulls utilities into your own CSS class:
CSS
@layer components {
.btn {
@apply rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700;
}
}I compiled this. .btn produced border-radius, background-color, padding-inline, padding-block and font properties, and the hover: utility was turned into a nested &:hover rule inside @media (hover: hover), as expected. The md:px-6 form also worked and produced a @media (width >= 48rem) block.
Use @apply for:
- Styling markup you do not control, such as HTML rendered from Markdown or a CMS
- Third-party widgets that need your styles
- A very small set of truly global elements
Avoid @apply as your main method. It brings back the problems utilities were meant to remove: naming things, jumping between files and growing CSS. Also, when you @apply a class that depends on theme or variants in a separately bundled file (such as Vue or Svelte style blocks, or CSS modules), import the main stylesheet with @reference:
CSS
@reference "../../app.css";Without it, those files cannot see your theme, so @apply will not work. (I checked that @reference "tailwindcss" allows @apply p-4 to compile.)
Option 4: Custom utilities with @utility
When you need a single-purpose class that Tailwind does not have, define a utility. It then works with every variant automatically.
CSS
@utility tab-4 {
tab-size: 4;
}I compiled this, and tab-4 worked, as did a variant combination like md:tab-4. In v4, @utility replaces the old pattern of adding custom classes inside @layer utilities. Custom utilities may also be functional (accepting a value), as the advanced course shows.
Option 5: Component classes in a design system
Large teams often combine a few CSS component classes (.btn, .card) for very stable elements with utilities for one-off layout. It is a valid choice as long as the team agrees on the rule: for example, "components for the design system's atoms, utilities for everything else".
Handling variants of a component
A button usually has sizes and colours. Do not build class strings by concatenating pieces. Tailwind only generates classes it can find as complete strings in your source, so this fails:
React (JSX)
// Broken: Tailwind cannot see "bg-red-600" in the source
const colour = "red";
<button className={`bg-${colour}-600`} />Instead, map the options to complete class names:
React (JSX)
const styles = {
primary: "bg-blue-600 text-white hover:bg-blue-700",
danger: "bg-red-600 text-white hover:bg-red-700",
};
function Button({ kind = "primary", children }) {
return <button className={`rounded-lg px-4 py-2 font-semibold ${styles[kind]}`}>{children}</button>;
}The next lessons cover this detection rule in more detail.
Merging conflicting classes
If a component accepts a className prop and the caller passes p-8 while the component has p-4, both classes exist, and the one that wins depends on the order in the generated CSS, not the order in the attribute. Libraries such as tailwind-merge resolve these conflicts, and clsx helps with conditional classes. The advanced course returns to this.
Try it yourself
Take a card with five repeated copies in a page. Extract a Card component, then add a kind prop with two complete class maps. Finally, write one @utility for something Tailwind lacks, and use it with a variant.
Summary
- Components are the main way to reuse Tailwind styles, because they share markup and classes together.
- Use
@applysparingly for content you cannot edit, and@utilityfor new single-purpose classes that work with variants. - Never build class names by string concatenation. Map options to complete class strings.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading