Color Mode

Introduction to Tailwind CSS

You have just finished reading about Bootstrap, a framework that gives you pre-built components and a structured grid system. Tailwind CSS takes a fundamentally different approach. There are no components, no grid classes, and no pre-designed buttons or cards waiting for you. Instead, Tailwind gives you a comprehensive set of low-level utility classes and lets you build everything yourself, directly in your HTML.

That might sound like more work, and in some ways it is. But it also means you never fight the framework. You never override styles you did not ask for. You never open a stylesheet to hunt down where a visual decision was made. Everything that affects how an element looks is right there in the HTML, visible at a glance.

This reading introduces Tailwind from the ground up: how to get it running, how its utility-first methodology works, how to write responsive and interactive styles, and how to extract repeated patterns into reusable abstractions using @apply.

What Makes Tailwind Different

The core idea behind Tailwind is that every CSS property you would ever write has a corresponding utility class. Instead of writing:


        .card {
            display: flex;
            flex-direction: column;
            padding: 1.5rem;
            background-color: white;
            border-radius: 0.5rem;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        }
    

You write the same thing directly on the element:


        <div class="flex flex-col p-6 bg-white rounded-lg shadow-sm">
            Card content
        </div>
    

The result is identical. The difference is where the styling lives. In the first example, you wrote CSS in a stylesheet and gave the element a semantic class name. In the second, you wrote the styles directly in the markup using Tailwind's utility classes. There is no stylesheet entry at all.

Utility-First vs Component-Based

Bootstrap takes a component-based approach: it ships with a button component, a card component, a navbar component. You use those components and customize them. Tailwind takes the opposite approach: it ships with no components at all. A button is whatever you build it to be, using utility classes. This is not a limitation. It is the point.

The practical consequence is that every design decision is explicit and local. When you look at a Tailwind-styled element, you see exactly why it looks the way it does. There is no inherited component style, no cascade to trace, no framework override needed. This makes Tailwind-based projects highly readable once you know the class names, and it makes customization trivial because there is nothing to override.

Why No Default Styles?

Tailwind ships with a CSS reset called Preflight that strips all browser default styles. Headings have no size or weight. Links have no underline. Lists have no bullets. This might feel disorienting at first, but it is intentional: Preflight gives you a completely blank canvas so your utility classes are the only thing determining how elements look. Nothing from the browser sneaks in unexpectedly.

The Design System Built In

One thing Tailwind does provide is a carefully considered design system. Its spacing scale, color palette, font sizes, and shadow values are not arbitrary. They are a curated set of options that work well together. When you use p-4 and p-8, those values have a meaningful ratio. When you use text-slate-700 and bg-slate-100, those colors are part of a coherent palette.

This matters because it constrains your choices in a productive way. Instead of picking from an infinite range of padding values, you pick from a scale. Your designs end up more consistent without requiring a separate design token system, because Tailwind's configuration file is your design token system.

Setup and Configuration

This is where Tailwind differs most sharply from Bootstrap in terms of setup. Bootstrap works fine via CDN with no build step required. Tailwind is fundamentally a build tool. Understanding why helps you understand how Tailwind works at a deeper level.

Why Tailwind Needs a Build Step

Tailwind's full library contains thousands of utility classes covering every combination of property, value, breakpoint, and state variant. If it shipped all of those classes in a single CSS file the way Bootstrap does, the file would be enormous. Unusable in production.

Instead, Tailwind scans your project files at build time and generates only the CSS for the classes you actually use. A project that uses flex, p-4, and text-blue-500 gets a CSS file containing exactly those three utilities and nothing else. This is called Just-In-Time (JIT) compilation, and it is how Tailwind keeps production CSS files tiny despite the enormous library behind it.

That scanning process requires knowing which files to look at. That is what the configuration file is for.

Installing Tailwind via npm

The standard installation uses npm and PostCSS, which you have worked with earlier in this course. Here is the full setup from scratch:


        npm install -D tailwindcss postcss autoprefixer
        npx tailwindcss init
    

The first command installs Tailwind, PostCSS, and Autoprefixer as development dependencies. The second generates a tailwind.config.js file in your project root. You will also need a PostCSS configuration file:


        // postcss.config.js
        export default {
            plugins: {
                tailwindcss: {},
                autoprefixer: {},
            },
        };
    

Then create your main CSS file with these three directives:


        /* src/input.css */
        @tailwind base;
        @tailwind components;
        @tailwind utilities;
    

These three @tailwind directives are where Tailwind injects its generated CSS during the build. base includes Preflight (the reset). components is where any extracted component classes land. utilities is where all your utility classes go. When Tailwind processes this file, it replaces these directives with the actual generated CSS.

To build your CSS, run:


        npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch
    

The --watch flag keeps the process running and rebuilds your CSS every time you save a file. Your HTML links to the generated output file, not the source file.

The Configuration File

The tailwind.config.js file is the heart of a Tailwind project. The most critical setting is content, which tells Tailwind which files to scan for class names:


        // tailwind.config.js
        export default {
            content: [
                './src/**/*.html',
                './src/**/*.js',
            ],
            theme: {
                extend: {
                    colors: {
                        brand: '#6d28d9',
                    },
                    fontFamily: {
                        sans: ['Inter', 'sans-serif'],
                    },
                },
            },
        };
    

The content array uses glob patterns to match files. If a class name appears in any matched file, Tailwind includes it in the output. If it does not appear in any matched file, it is not included. This is why the content paths must be accurate: a missing path means Tailwind cannot find your classes and they will not appear in the generated CSS.

The theme.extend object is where you add custom values to Tailwind's design system without replacing the defaults. Adding a brand color here makes text-brand, bg-brand, and border-brand available as utility classes throughout your project.

Never Build Class Names Dynamically

Because Tailwind scans your source files for class names as plain text strings, dynamically constructed class names will not be found. A string like `text-${color}-500` in JavaScript will not be detected. Always use complete class names in your source files. If you need conditional classes, list both possibilities explicitly so Tailwind can find them both.

The CDN Option (For Learning Only)

Tailwind does provide a CDN script for quick prototyping and learning. Unlike Bootstrap's CDN, which serves a static pre-built file, Tailwind's CDN version runs the JIT engine directly in the browser using a JavaScript bundle. It detects class names in your HTML in real time and generates the CSS on the fly.


        <script src="https://cdn.tailwindcss.com"></script>
    

This works, and it is genuinely useful for learning and quick experiments. But Tailwind explicitly labels this as not suitable for production. The in-browser JIT engine adds significant JavaScript overhead, the generated CSS cannot be cached or optimized, and you lose the ability to use a config file for customization. Use it to learn. Do not use it to ship.

Writing Tailwind: Utility Classes in Practice

Once Tailwind is configured and your build is running, you write styles by adding classes directly to your HTML. The class names follow predictable patterns that map closely to CSS property names and values.

Class Name Patterns

Most Tailwind utilities follow a property-value pattern. flex sets display: flex. flex-col sets flex-direction: column. items-center sets align-items: center. gap-4 sets gap: 1rem. Once you learn the pattern, you can often guess a class name correctly before looking it up.

Spacing uses a numeric scale where each step is 0.25rem. So p-1 is 0.25rem, p-4 is 1rem, p-8 is 2rem, and p-16 is 4rem. The same scale applies to margins, gaps, widths, heights, and more. Learning the spacing scale once pays off across all layout utilities.


        <!-- A simple card built entirely with Tailwind utilities -->
        <div class="flex flex-col gap-3 p-6 bg-white rounded-xl shadow-md">
            <img
                src="..."
                alt="..."
                class="w-full h-48 object-cover rounded-lg"
            >
            <h2 class="text-xl font-bold text-slate-900">
                Card Title
            </h2>
            <p class="text-slate-600 leading-relaxed">
                Supporting description text for the card goes here.
            </p>
            <button class="mt-auto px-4 py-2 bg-blue-600 text-white rounded-lg font-medium">
                Read More
            </button>
        </div>
    

Notice there is no stylesheet entry anywhere. The card's entire appearance is defined by the classes on each element. The mt-auto on the button pushes it to the bottom of the flex column regardless of how much text is above it. leading-relaxed sets a comfortable line height. object-cover prevents the image from distorting. Each class does one thing, and together they describe the complete visual output.

Colors and the Tailwind Palette

Tailwind's color system uses a two-part naming convention: a color name and a shade number. blue-600, slate-100, emerald-500. Shade numbers run from 50 (very light) to 950 (very dark) in steps of 100. The same color name works across background, text, border, and other color utilities:


        <div class="bg-slate-100 border border-slate-300 text-slate-800">
            Consistent color palette, three utilities
        </div>
    

Responsive Variants

Tailwind handles responsive design with breakpoint prefixes added to any utility class. The breakpoints are sm (640px), md (768px), lg (1024px), xl (1280px), and 2xl (1536px). Like Bootstrap, Tailwind is mobile-first: unprefixed classes apply at all sizes, prefixed classes apply from that breakpoint upward.


        <!-- Stacked on mobile, side by side on medium screens and up -->
        <div class="flex flex-col md:flex-row gap-4">
            <div class="w-full md:w-1/3">Sidebar</div>
            <div class="w-full md:w-2/3">Main content</div>
        </div>
    

The prefix syntax is simple: add the breakpoint name followed by a colon before the utility class. md:flex-row means "apply flex-direction: row at medium screens and up." You can stack multiple prefixes on a single element, and each prefixed class is independent.

State Variants

The same prefix syntax works for interactive states. hover:, focus:, active:, disabled:, and many others let you style elements in response to user interaction without writing a single line of CSS.


        <button class="
            px-4 py-2
            bg-blue-600
            hover:bg-blue-700
            focus:outline-none
            focus:ring-2
            focus:ring-blue-500
            focus:ring-offset-2
            active:bg-blue-800
            disabled:opacity-50
            disabled:cursor-not-allowed
            text-white rounded-lg font-medium
            transition-colors duration-200
        ">
            Submit
        </button>
    

This button has hover, focus, active, and disabled states defined entirely in its class list. The transition-colors and duration-200 classes add a smooth color transition. Notice that long class lists like this are common in Tailwind. Many developers use multi-line formatting as shown above, or a Prettier plugin that automatically sorts and formats Tailwind classes.

Combining Variants

Variants can be combined. md:hover:bg-blue-700 applies the hover background only at medium screens and up. dark:hover:bg-slate-700 applies a hover color only when dark mode is active. This composability is one of Tailwind's most powerful features, and it is something that would require significant CSS complexity to replicate manually.

Building a Component System with @apply

Writing utilities directly in HTML is powerful for one-off elements, but real projects have patterns that repeat everywhere: cards, buttons, navigation links, form inputs, badges, alerts. Writing 20 utility classes on every card in a 50-page project is not maintainable. This is where @apply becomes one of Tailwind's most important features.

@apply lets you collect dozens of Tailwind utility classes into a single named CSS class. The result is a proper component class, authored entirely in terms of Tailwind's design system, that you can use with a single class name anywhere in your project. This is how you build a component library in Tailwind without leaving the Tailwind ecosystem.

The History of @apply

@apply was originally proposed as a native CSS specification. The idea was that any developer could write a rule in a plain stylesheet and then apply those declarations to another selector using @apply, without any framework involved. The specification was drafted, discussed in the CSS working group, and ultimately abandoned. Browser vendors could not reach agreement on the cascade and specificity semantics, and the proposal was withdrawn from the standards track.

Tailwind kept the syntax and implemented it as a build-time transformation through PostCSS. When PostCSS processes your CSS file, it finds every @apply directive and replaces it with the actual CSS declarations from the named utilities. By the time the browser sees the stylesheet, every @apply directive has been resolved into plain, standard CSS. The browser never sees @apply at all. It was never a browser feature to begin with.

This is why @apply only works in a Tailwind build environment. It requires the PostCSS plugin to process it. Drop it into a plain CSS file with no build step and nothing will happen. It is a Tailwind-specific authoring tool, not a CSS feature.

Building a Real Card Component

Here is what building a complete card component with @apply looks like in practice. A real card has structure: a container, an image, a body, a title, body text, and a footer with actions. Each of those elements needs its own class with its own set of utilities.


        @layer components {

            .card {
                @apply flex flex-col overflow-hidden rounded-2xl shadow-md;
                @apply bg-white border border-slate-200;
                @apply transition-shadow duration-200 hover:shadow-lg;
            }

            .card-image {
                @apply w-full h-52 object-cover;
            }

            .card-body {
                @apply flex flex-col flex-1 gap-3 p-6;
            }

            .card-badge {
                @apply self-start px-3 py-1 rounded-full text-sm font-semibold;
                @apply bg-blue-100 text-blue-700;
            }

            .card-title {
                @apply text-xl font-bold text-slate-900 leading-snug;
            }

            .card-text {
                @apply text-slate-600 leading-relaxed flex-1;
            }

            .card-footer {
                @apply flex items-center justify-between pt-4 mt-auto;
                @apply border-t border-slate-100;
            }

        }
    

With those component classes defined, the HTML becomes clean and readable:


        <article class="card">
            <img class="card-image" src="..." alt="...">
            <div class="card-body">
                <span class="card-badge">Design</span>
                <h2 class="card-title">Article Title Goes Here</h2>
                <p class="card-text">
                    Supporting description text for the article.
                </p>
                <div class="card-footer">
                    <span class="text-sm text-slate-400">5 min read</span>
                    <a href="#" class="btn-primary">Read More</a>
                </div>
            </div>
        </article>
    

That HTML is as clean as Bootstrap's card markup. But unlike Bootstrap's card, every style decision behind it is something you wrote using Tailwind utilities. The design system is yours. The spacing, colors, radius, shadow, and hover behavior all come from Tailwind's scale, so they are consistent with everything else in the project. And when you need to update the card, you update the @layer components block in one place.

Building a Complete Button System

Buttons are another perfect candidate for @apply. A real button is not just background color and padding: it has hover, focus, active, and disabled states. It may have size variants. Writing all of that inline every time a button appears in the project is where utility-first breaks down without @apply.


        @layer components {

            /* Base button: shared structure and behavior across all variants */
            .btn {
                @apply inline-flex items-center justify-center gap-2;
                @apply px-4 py-2 rounded-lg font-medium text-sm;
                @apply transition-all duration-200 cursor-pointer;
                @apply focus:outline-none focus:ring-2 focus:ring-offset-2;
                @apply disabled:opacity-50 disabled:cursor-not-allowed;
            }

            /* Variants */
            .btn-primary {
                @apply btn bg-blue-600 text-white;
                @apply hover:bg-blue-700 active:bg-blue-800;
                @apply focus:ring-blue-500;
            }

            .btn-secondary {
                @apply btn bg-slate-100 text-slate-800;
                @apply hover:bg-slate-200 active:bg-slate-300;
                @apply focus:ring-slate-400;
            }

            .btn-danger {
                @apply btn bg-red-600 text-white;
                @apply hover:bg-red-700 active:bg-red-800;
                @apply focus:ring-red-500;
            }

            /* Size modifiers */
            .btn-sm {
                @apply px-3 py-1.5 text-xs;
            }

            .btn-lg {
                @apply px-6 py-3 text-base;
            }

        }
    

Notice that .btn-primary applies btn itself using @apply. You can apply your own component classes inside other component classes. The base .btn class holds all the structural and behavioral styles shared across variants, and each variant adds only what is unique to it. In HTML:


        <button class="btn-primary">Save Changes</button>
        <button class="btn-secondary">Cancel</button>
        <button class="btn-danger btn-sm">Delete</button>
        <button class="btn-primary btn-lg">Get Started</button>
    

This is a fully functional button system: four lines of HTML, zero inline utility classes, complete interactive state coverage, and a design that comes entirely from Tailwind's scale. That is the real power of @apply.

Using @layer to Keep Utilities in Control

All component classes built with @apply belong inside an @layer components block. This is not optional. Tailwind uses CSS cascade layers to ensure that utility classes always have higher priority than component classes. The three layers, in order of increasing priority, are base, components, and utilities.

The practical consequence is that you can always override a component class with a utility class without needing !important. If you need one specific button to be wider, you add w-full alongside btn-primary and it wins because utilities layer beats components layer. This keeps component classes as good defaults while preserving the flexibility to adjust individual instances inline.


        <!-- Standard button -->
        <button class="btn-primary">Save</button>

        <!-- Full-width on this specific form, utility overrides component -->
        <button class="btn-primary w-full">Create Account</button>

        <!-- Different text color on this one instance, utility wins -->
        <button class="btn-secondary text-blue-600">Learn More</button>
    

What Belongs in @apply

Not everything should become a component class. The guideline is straightforward: if a pattern repeats across the project with the same set of utilities, and changing it means hunting down every instance, it belongs in @apply. If it appears once or twice, leave it in the HTML.

Good candidates for component classes include buttons and their variants, card structures and their sub-elements, form inputs and labels, navigation links, badges and tags, and alert or notification patterns. These are the elements every project has dozens or hundreds of. Bad candidates include one-off hero sections, page-specific layout wrappers, and anything that only appears in one place in the entire project.

@apply Gives You the Best of Both Worlds

The combination of utility classes and @apply components gives you something neither approach delivers alone. Utilities give you precision and speed for one-off styling. Component classes give you reusability and maintainability for repeated patterns. Using both strategically means your HTML stays clean where it matters, your design system stays consistent, and every style still ultimately traces back to Tailwind's configuration rather than hand-authored magic numbers in a stylesheet.

Bootstrap and Tailwind Side by Side

You have now seen both frameworks in enough depth to compare them meaningfully. They solve the same problem (building UIs without writing all your CSS from scratch) but with fundamentally different philosophies.

What Each Framework Gives You

Bootstrap ships with a complete component library. Buttons, cards, modals, navbars, and dozens more are ready to use the moment you add the CDN link. The tradeoff is that your project starts with a large, opinionated stylesheet and you customize from there. Bootstrap is excellent when you need to move fast, when the default component styles are close to what you need, or when you are working in a team that already knows Bootstrap conventions.

Tailwind ships with nothing but a design system and utilities. There are no pre-built components. Every visual pattern is something you construct. The tradeoff is that you write more markup upfront but end up with a codebase where every style decision is explicit and local. Tailwind is excellent when you need precise control over design, when the default Bootstrap aesthetic would require heavy overriding, or when you are building a design system from scratch.

The Same Button, Two Approaches

Here is the same primary button written in each framework. The visual output is similar. The approach is completely different.


        <!-- Bootstrap -->
        <button class="btn btn-primary">
            Save Changes
        </button>

        <!-- Tailwind -->
        <button class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors">
            Save Changes
        </button>
    

The Bootstrap button gets its appearance from Bootstrap's pre-written .btn and .btn-primary rules. If you want it to look different, you override those rules. The Tailwind button has no pre-written rules. Its appearance is entirely the product of the classes you added. If you want it to look different, you change the classes.

File Size and Build Process

Bootstrap's minified CSS is around 230KB before gzip. You get all of it whether you use it or not. With a CDN, this is cached and fast, but the file is a fixed cost.

A Tailwind production build typically generates 5 to 15KB of CSS for a real project, because only the classes you use are included. The tradeoff is that this requires a build step. There is no "just drop in a CDN link and have everything" with Tailwind in production.

Which One Should You Use?

The honest answer is that it depends on the project. Neither framework is universally better. Bootstrap is faster to start with and better when you need pre-built interactive components without much customization. Tailwind is better when design precision matters, when you want to avoid fighting a component library, or when file size is a concern.

What matters for your development as a CSS professional is understanding both approaches well enough to make that choice deliberately, rather than defaulting to whichever one you learned first.

Try It Yourself

To get hands-on experience with Tailwind, the CDN approach is the fastest path for experimentation. Create a plain HTML file and add the Tailwind CDN script:


        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Tailwind Practice</title>
            <script src="https://cdn.tailwindcss.com"></script>
        </head>
        <body class="p-8 bg-slate-50">
            <!-- Your content here -->
        </body>
        </html>
    

With that in place, try building the following:

Keep the Tailwind documentation open while you work. The search function is fast and the docs show the exact CSS each class generates, which is useful when you are learning what the classes actually do.

Explore Independently

You do not need to submit this work. The goal is to feel the difference between Tailwind's approach and Bootstrap's. Pay attention to what feels different, what feels easier, and what feels harder. You will form opinions about these frameworks, and those opinions will sharpen as you use both in the upcoming assignment.