Chapter 6
Tailwind CSS and Bootstrap
Utility-first versus component frameworks, configuration and trade-offs.
6. Tailwind CSS and Bootstrap
Tailwind CSS and Bootstrap both help teams build interfaces without writing every visual rule from scratch, but they solve the problem from different directions. Tailwind provides low-level utility classes that compose directly in markup. Bootstrap combines a responsive layout system, utilities, and ready-made component styles with a theme and JavaScript plugins.
Neither approach removes the need to understand CSS. Both generate styles that follow the cascade, box model, responsive layout, and accessibility principles from earlier chapters. The right choice depends on the product, existing stack, design requirements, team skills, and how much control the team wants over component behavior.
Overview & Objectives
By the end, you will be able to choose between these approaches, build a responsive pricing layout in each, and diagnose production styles missing because of content scanning.
Utility-First CSS and Component Frameworks
Traditional CSS might define a .pricing-card class with padding, border, radius, and background rules. Utility-first CSS provides small classes for those declarations, composed in the template:
<article class="rounded-xl border border-slate-200 bg-white p-6">
...
</article>The utilities act like a constrained vocabulary. Spacing, colors, type sizes, and breakpoints commonly map to a design scale rather than arbitrary values. This can make styling local to the component markup and reduce one-off CSS naming decisions.
Bootstrap also provides utilities, but it additionally includes a predefined grid, Reboot base styles, Sass variables and maps, and component styles such as buttons, cards, and navbars. Its components establish a recognizable baseline that projects can customize. Bootstrap's JavaScript plugins are optional; the CSS and grid can be used without loading the interactive JavaScript bundle.
These categories are not absolute: Tailwind supports reusable application components, and Bootstrap can be themed. Loading both systems globally can add CSS weight and competing design tokens.
Where should reusable UI live?
Reuse meaningful structure and behavior in application components rather than duplicating complex markup:
type PricingCardProps = {
name: string;
price: string;
description: string;
};
function PricingCard({ name, price, description }: PricingCardProps) {
return (
<article className="rounded-xl border border-slate-200 bg-white p-6">
<h2 className="text-xl font-semibold">{name}</h2>
<p className="mt-2 text-slate-600">{description}</p>
<p className="mt-6 text-3xl font-bold">{price}</p>
</article>
);
}The component provides a semantic boundary; utilities express its styling. Bootstrap projects can use the same pattern. Extract components for repeated structure or behavior, not merely to shorten a class attribute.
Tailwind CSS Architecture
Tailwind scans configured source files for class names and generates CSS for recognized utilities. The application includes the generated stylesheet; it does not ship a runtime engine that inspects every element in the browser and constructs styles on demand.
This on-demand class detection is often called JIT (just-in-time) generation. Tailwind generates CSS for candidates found in project sources. Configuration APIs differ between major versions, so use documentation for the installed version.
The CSS pipeline
A stylesheet entry point and build plugin let Tailwind scan configured sources, resolve class candidates against utilities and theme rules, then emit CSS for the normal bundler. A class must be statically discoverable; a runtime string assembled from fragments may not produce CSS.
// Risky: the scanner may not see the complete class candidates.
const className = `bg-${tone}-600`;Prefer a static mapping:
const toneClasses = {
blue: "bg-blue-600 hover:bg-blue-700",
amber: "bg-amber-600 hover:bg-amber-700",
rose: "bg-rose-600 hover:bg-rose-700",
} as const;
const className = toneClasses[tone];Complete candidates are visible to the scanner. For truly variable values, use a validated style or CSS custom property instead of constructing unbounded class names.
Responsive and state variants
Tailwind variants prefix a utility with the condition under which it applies. Common examples include responsive breakpoints and interaction states:
<button class="w-full rounded-lg bg-blue-700 px-4 py-3 font-semibold text-white hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 md:w-auto">
Choose plan
</button>Unprefixed classes are the base; md: applies at the configured breakpoint and above. hover: and focus-visible: style interaction states. Check breakpoint names against the installed version, and retain visible keyboard focus. Variants can also respect preferences such as reduced motion.
Design tokens and theme configuration
Tailwind's theme supplies design tokens for spacing, colors, typography, breakpoints, radii, and shadows. The setup varies by major version: some use a JavaScript/TypeScript config, while newer workflows may define tokens in CSS. Do not assume their APIs are interchangeable.
An illustrative configuration concept is:
// Illustrative legacy-style config shape; check the installed Tailwind version.
export default {
theme: {
extend: {
colors: {
brand: {
700: "#174a8b",
800: "#103866",
},
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
},
},
},
};In versions using this configuration model, the values create utilities such as bg-brand-700. Centralize approved values under stable names like brand, surface, or danger; token changes can affect many components and deserve system-wide review.
Arbitrary values: useful escape hatch, not a new token system
Arbitrary values allow a one-off CSS value to be expressed when the design genuinely needs it:
<div class="max-w-[68ch]">
...
</div>Use arbitrary values sparingly. Promote repeated values to tokens; accumulating near-duplicates undermines a consistent theme.
@apply: Trade-offs and Alternatives
@apply composes utility declarations into a CSS rule in Tailwind-supported processing contexts:
/* Example syntax; ensure this stylesheet is processed by the installed Tailwind version. */
.button-primary {
@apply rounded-lg bg-blue-700 px-4 py-3 font-semibold text-white;
}@apply can share a small global pattern or ease migration, but may hide composition, create a parallel naming system, and complicate variants. Prefer application components for semantic UI and utilities for understandable local styles. Use @apply for a concrete CSS reuse need, and remember its declarations still participate in layers, specificity, and source order.
Bootstrap 5 Architecture
Bootstrap 5 is a CSS and JavaScript framework with a responsive grid, Reboot, utility classes, Sass customization, and optional interactive components.
Reboot
Reboot normalizes selected browser defaults and establishes base behavior; it is not a complete design system. It can change native elements across an existing app, so check its interaction with any current reset before importing it.
The responsive grid
Bootstrap 5's classic grid uses containers, rows, and columns. It is based on a 12-column system, with gutters and responsive breakpoint classes:
<div class="container">
<div class="row g-4">
<div class="col-12 col-md-6 col-lg-4">...</div>
<div class="col-12 col-md-6 col-lg-4">...</div>
<div class="col-12 col-md-6 col-lg-4">...</div>
</div>
</div>Columns stack on small screens, become two per row at medium, and three at large widths. The grid does not replace semantic HTML or source order. Breakpoints are customizable via Sass; check the project's Bootstrap 5 version and configuration.
Utility classes and components
Bootstrap utilities cover display, spacing, color, typography, flexbox, sizing, and responsive variants. CSS components provide a visual baseline; optional JavaScript plugins add behaviors such as dropdowns and modals. Check semantics, keyboard support, focus, and labels—framework styling alone does not guarantee accessibility. Bootstrap 5 does not require jQuery; import only needed plugins, or omit JavaScript when using just grid and utilities.
Sass customization
Bootstrap Sass variables and maps customize generated styles and must be set before importing the relevant framework files. A compiled CDN stylesheet cannot be changed with Sass; use supported CSS custom properties or compile Sass through the build. Keep customization versioned and reproducible.
Responsive Pricing Table in Tailwind
This pricing example uses semantic cards, a responsive grid, visible feature lists, and an explicit focus treatment. The plan names and prices are sample content; real prices, terms, and checkout destinations must come from the product's approved source.
<section class="mx-auto max-w-6xl px-4 py-12 sm:px-6 lg:px-8" aria-labelledby="plans-heading">
<header class="mx-auto max-w-2xl text-center">
<h1 id="plans-heading" class="text-3xl font-bold tracking-tight text-slate-950 sm:text-4xl">
Plans for every team
</h1>
<p class="mt-4 text-lg text-slate-600">
Start with the essentials and choose a plan that fits your work.
</p>
</header>
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
<article class="flex flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<h2 class="text-xl font-semibold text-slate-950">Starter</h2>
<p class="mt-2 text-slate-600">For individual learners.</p>
<p class="mt-6 text-4xl font-bold text-slate-950">
$0 <span class="text-base font-normal text-slate-600">/ month</span>
</p>
<ul class="mt-6 flex-1 space-y-3 text-slate-700">
<li>Core learning materials</li>
<li>Progress saved in this browser</li>
</ul>
<a
class="mt-8 inline-flex min-h-11 items-center justify-center rounded-lg border border-blue-800 px-4 py-2 font-semibold text-blue-800 hover:bg-blue-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-800"
href="/signup?plan=starter"
>
Get started
</a>
</article>
<article class="flex flex-col rounded-2xl border-2 border-blue-800 bg-white p-6 shadow-sm">
<p class="text-sm font-semibold uppercase tracking-wide text-blue-800">Popular</p>
<h2 class="mt-2 text-xl font-semibold text-slate-950">Team</h2>
<p class="mt-2 text-slate-600">For a growing team.</p>
<p class="mt-6 text-4xl font-bold text-slate-950">
$24 <span class="text-base font-normal text-slate-600">/ member / month</span>
</p>
<ul class="mt-6 flex-1 space-y-3 text-slate-700">
<li>Everything in Starter</li>
<li>Shared team workspaces</li>
</ul>
<a
class="mt-8 inline-flex min-h-11 items-center justify-center rounded-lg bg-blue-800 px-4 py-2 font-semibold text-white hover:bg-blue-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-800"
href="/signup?plan=team"
>
Choose Team
</a>
</article>
<article class="flex flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<h2 class="text-xl font-semibold text-slate-950">Business</h2>
<p class="mt-2 text-slate-600">For organizations.</p>
<p class="mt-6 text-4xl font-bold text-slate-950">Contact us</p>
<ul class="mt-6 flex-1 space-y-3 text-slate-700">
<li>Everything in Team</li>
<li>Custom onboarding</li>
</ul>
<a
class="mt-8 inline-flex min-h-11 items-center justify-center rounded-lg border border-blue-800 px-4 py-2 font-semibold text-blue-800 hover:bg-blue-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-800"
href="/contact"
>
Talk to sales
</a>
</article>
</div>
</section>grid-cols-1 stacks the plans; md:grid-cols-3 creates three columns at medium widths. Flex cards and flex-1 align actions without fixed heights. The minimum target height and focus outline support interaction; verify contrast and links in the actual product.
Equivalent Pricing Table in Bootstrap 5
The same content and hierarchy can be implemented with Bootstrap's container, row, and column classes. Bootstrap's CSS must be installed or imported through the application build for these classes to have an effect.
<section class="container py-5" aria-labelledby="plans-heading">
<header class="mx-auto text-center" style="max-width: 42rem;">
<h1 id="plans-heading" class="display-5 fw-bold">Plans for every team</h1>
<p class="lead text-secondary">
Start with the essentials and choose a plan that fits your work.
</p>
</header>
<div class="row row-cols-1 row-cols-md-3 g-4 mt-3">
<div class="col">
<article class="card h-100 shadow-sm">
<div class="card-body d-flex flex-column p-4">
<h2 class="h4 card-title">Starter</h2>
<p class="text-secondary">For individual learners.</p>
<p class="display-6 fw-bold">$0 <span class="fs-6 fw-normal">/ month</span></p>
<ul class="list-unstyled flex-grow-1">
<li class="mb-2">Core learning materials</li>
<li class="mb-2">Progress saved in this browser</li>
</ul>
<a class="btn btn-outline-primary mt-3" href="/signup?plan=starter">Get started</a>
</div>
</article>
</div>
<div class="col">
<article class="card h-100 border-primary shadow-sm">
<div class="card-body d-flex flex-column p-4">
<p class="small fw-bold text-primary text-uppercase">Popular</p>
<h2 class="h4 card-title">Team</h2>
<p class="text-secondary">For a growing team.</p>
<p class="display-6 fw-bold">$24 <span class="fs-6 fw-normal">/ member / month</span></p>
<ul class="list-unstyled flex-grow-1">
<li class="mb-2">Everything in Starter</li>
<li class="mb-2">Shared team workspaces</li>
</ul>
<a class="btn btn-primary mt-3" href="/signup?plan=team">Choose Team</a>
</div>
</article>
</div>
<div class="col">
<article class="card h-100 shadow-sm">
<div class="card-body d-flex flex-column p-4">
<h2 class="h4 card-title">Business</h2>
<p class="text-secondary">For organizations.</p>
<p class="display-6 fw-bold">Contact us</p>
<ul class="list-unstyled flex-grow-1">
<li class="mb-2">Everything in Team</li>
<li class="mb-2">Custom onboarding</li>
</ul>
<a class="btn btn-outline-primary mt-3" href="/contact">Talk to sales</a>
</div>
</article>
</div>
</div>
</section>row-cols-1 row-cols-md-3 controls columns and g-4 gutters; flex utilities align the actions. Move custom values such as the inline maximum width into project CSS or Sass. Both examples share structure but neither framework supplies pricing semantics: preserve meaningful HTML and reading order.
Architecture and Team Trade-offs
| Consideration | Tailwind CSS | Bootstrap 5 |
|---|---|---|
| Primary workflow | Compose low-level utilities in templates/components | Use a predefined grid, utilities, and component styles |
| Design freedom | High; the team defines the visual system | Starts with a recognizable framework theme, customizable through Sass/CSS |
| Component behavior | Usually owned by the app or a separate component library | CSS components plus optional JavaScript plugins |
| CSS naming | Few custom class names required for one-off layout | Framework class vocabulary plus custom classes |
| Markup | May contain many utility classes | Uses semantic framework classes such as row, card, and btn |
| Design consistency | Theme scales and component APIs must be curated | Strong baseline; custom theme still needs ownership |
| Adoption | Requires learning utility conventions and build integration | Familiar patterns and docs; requires aligning product design with framework choices |
| Risk | Arbitrary values and duplicated long patterns can drift | Overriding framework styles and unused plugins can add complexity |
Tailwind suits custom design systems and teams that want styles close to application components. It requires conventions for readable class composition and shared patterns. Bootstrap suits teams that want a mature grid and component baseline, or already maintain a Bootstrap app; distinctive branding may require more theming and override discipline.
Choose based on existing accessible components, design requirements, team familiarity, reset interactions, required JavaScript, and how tokens and CSS enter the build. Avoid loading both frameworks globally; if migration requires both, scope ownership by page or component to limit conflicting resets and tokens.
Production CSS, Content Scanning, and Purging
Tailwind: static class detection
Production output depends on scanning every relevant source file. Ignored directories, external packages, and dynamic class strings can cause styles to disappear. Confirm the installed version's source configuration includes templates, use complete static strings or finite mappings, and inspect production CSS. Safelist only when necessary; broad patterns can bloat output.
Bootstrap: Sass modules and unused CSS
Precompiled Bootstrap CSS includes many rules whether or not markup uses them. Sass imports can reduce output, but have dependencies and order requirements. JavaScript tree-shaking is separate from CSS pruning: import only used plugins and use documented CSS optimization, then verify production output.
A reliable production check
Build with deployment settings, open key routes, and exercise responsive and conditional variants. Check expected selectors in generated CSS and inspect network requests for missing or duplicate styles. Never turn unvalidated user input into unbounded class names.
Debugging Workflow
- Classify the issue: missing stylesheet or generated class, cascade conflict, layout, or interaction.
- Inspect computed styles and their source in DevTools.
- For Tailwind, check source scanning, class construction, and production selectors. For Bootstrap, check the CSS import and reset/override conflicts.
- Reproduce in production; test keyboard focus and responsive variants after changing the cause.
Accessibility & Best Practices
- Preserve semantic HTML, source order, and accessible behavior independently of class names.
- Use shared design tokens; avoid arbitrary values that drift.
- Extract components for repeated structure or behavior.
- Keep visible keyboard focus and test contrast after theming.
- Load only the reset and JavaScript plugins the app needs.
- Keep Tailwind classes statically detectable; customize Bootstrap through supported Sass or scoped styles.
- Verify production CSS, responsive behavior, and interactions.
Practice Exercises
Exercise 1 — Diagnose a missing Tailwind production style
This component's background style works for the blue variant but is missing for amber in the production build:
function Status({ tone }: { tone: "blue" | "amber" }) {
return <span className={`rounded px-2 py-1 bg-${tone}-100`}>Status</span>;
}Rewrite the component so all supported background and text colors are statically discoverable. Describe how you would check that the source file is included in the Tailwind scan and verify the result in a production build.
Exercise 2 — Adapt a Bootstrap plan row
A Bootstrap 5 pricing row should show one card per row on small screens, two per row at the medium breakpoint, and three per row at the large breakpoint. The cards should have equal height, their action links should align at the bottom, and those links need a visible keyboard focus state. Provide the Bootstrap markup and explain which styles come from the grid, which from flex utilities, and which accessibility behaviors must be checked separately.
Solutions
Solution 1
const toneClasses = {
blue: "bg-blue-100 text-blue-900",
amber: "bg-amber-100 text-amber-900",
} as const;
function Status({ tone }: { tone: keyof typeof toneClasses }) {
return (
<span className={`rounded px-2 py-1 ${toneClasses[tone]}`}>
Status
</span>
);
}Each complete utility string is present in source for the scanner. Confirm the Tailwind source/content configuration covers the directory containing this component, and inspect the production CSS for both generated background and text selectors. Then run the production build and verify both variants in a browser, including contrast and state behavior. Avoid adding a broad safelist until the scan configuration and source class strategy have been checked.
Solution 2
<div class="container">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<div class="col">
<article class="card h-100">
<div class="card-body d-flex flex-column">
<h2 class="h4 card-title">Starter</h2>
<p class="card-text">For individual learners.</p>
<a class="btn btn-outline-primary mt-auto" href="/plans/starter">Choose Starter</a>
</div>
</article>
</div>
<div class="col">
<article class="card h-100">
<div class="card-body d-flex flex-column">
<h2 class="h4 card-title">Team</h2>
<p class="card-text">For a growing team.</p>
<a class="btn btn-primary mt-auto" href="/plans/team">Choose Team</a>
</div>
</article>
</div>
<div class="col">
<article class="card h-100">
<div class="card-body d-flex flex-column">
<h2 class="h4 card-title">Business</h2>
<p class="card-text">For organizations.</p>
<a class="btn btn-outline-primary mt-auto" href="/contact">Contact sales</a>
</div>
</article>
</div>
</div>
</div>row-cols-1, row-cols-md-2, and row-cols-lg-3 define the responsive column counts; g-4 supplies gutters. h-100 lets each card fill its grid column's height. d-flex flex-column makes the card body a vertical flex container, and mt-auto pushes the action link toward its bottom.
The anchor is navigation, so it has a real href. Verify the actual Bootstrap theme keeps keyboard focus visible, the button-like link has sufficient contrast, the target is comfortably sized, and the heading hierarchy is meaningful. A CSS framework provides styling, but the application remains responsible for accessible names, logical source order, focus behavior, and truthful pricing content.