Tailwind CSS and React: A Practical Setup Guide
Tailwind CSS takes a different path from component-driven frameworks like Bootstrap or Foundation. Instead of shipping predefined components that impose design decisions, it provides low-level utility classes that let you compose your own styles directly in your markup. This makes it a strong fit for developers who want full control over their interfaces without fighting a framework's built-in assumptions.
Before deciding whether Tailwind is right for you, consider how you work. The utility-first model means no more inventing class names or wrestling with CSS specificity. Since you reuse the same classes across your markup, small design tweaks don't force you to bust your CSS cache. But Tailwind isn't ideal in every situation. For quick mini-projects with tight deadlines, a framework with ready-made components might serve you better. If you're still learning CSS fundamentals, Tailwind's utility classes assume you understand the underlying properties. And if you dislike markup cluttered with many class attributes, you may prefer writing custom CSS or using a more traditional framework.
This guide walks through setting up Tailwind CSS in a React project built with create-react-app, then fitting the pieces together to produce a profile card using nothing but Tailwind utility classes.
Scaffolding the React Project
If you don't already have a React app, scaffold one with create-react-app:
npx create-react-app react-tailwindcss && cd react-tailwindcss
Next, install Tailwind CSS and its required build tools as development dependencies. Use npm or yarn depending on your preference:
npm install tailwindcss postcss-cli [email protected] -D
yarn add tailwindcss postcss-cli autoprefixer -D
Now initialize Tailwind's default configuration. This creates a tailwind.js file in your project root:
npx tailwind init tailwind.js --full
The tailwind.js file holds your project's design tokens: colors, themes, media queries, and other configuration options. It's where you'd rebrand or adjust conventions later if needed.
Setting Up PostCSS and Autoprefixer
PostCSS transforms your styles through JavaScript plugins. Tailwind itself is a PostCSS plugin, and you'll want Autoprefixer alongside it because Tailwind does not handle vendor prefixing. Autoprefixer checks caniuse.com to determine which CSS properties need prefixes.
Create a PostCSS configuration file in your project's base directory:
touch postcss.config.js
Add the Tailwind and Autoprefixer plugins to that file:
const tailwindcss = require('tailwindcss');
module.exports = {
plugins: [
tailwindcss('./tailwind.js'),
require('autoprefixer')
],
};
Here's what the configuration does:
- Loads the Tailwind CSS package into a variable.
- Passes your
tailwind.jsconfig file into thetailwindcssplugin. - Loads the
autoprefixerpackage as a second plugin.
Injecting Tailwind's Styles into Your App
Inside your src folder, create an assets directory. Here you'll add two CSS files: tailwind.css for importing Tailwind styles and writing custom rules, and main.css which will hold the compiled output generated from tailwind.css.
Add the following to your tailwind.css file:
@tailwind base;
@tailwind components;
@tailwind utilities;
The @tailwind directive tells Tailwind which style layers to inject:
@tailwind baseinjects Tailwind's base styles, combining Normalize.css with additional resets.@tailwind componentsinjects any component classes registered by plugins based on your config file.@tailwind utilitiesinjects all of Tailwind's utility classes, both defaults and any custom ones you define.
Tailwind swaps these directives out at build time with the actual generated CSS. If you use postcss-import in your setup, use this alternative syntax instead:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
Building CSS on Start
To ensure your styles are recompiled whenever you run your dev server, update the scripts section of your package.json:
"scripts": {
"start": "npm run watch:css && react-scripts start",
"build": "npm run watch:css && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"watch:css": "postcss src/assets/tailwind.css -o src/assets/main.css"
}
This configuration rebuilds your CSS each time you run npm start or yarn start.
Importing the Compiled CSS
Now wire up the imports so the generated styles are picked up correctly. Open your index.js file and make these changes:
- Import
main.cssinstead of the defaultindex.css. Yourindex.jsshould look like this after the change:
import './assets/main.css'
import React from "react";
import ReactDOM from "react-dom";
import './assets/main.css';
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
- In
App.js, remove thelogo.svgimport and clear out the contents of theAppcomponent. Don't worry about the empty state — you'll fill it in shortly.
Building the Profile Card
With the setup complete, start your dev server with npm start or yarn start. You'll see Tailwind compiling the necessary files into main.css.
Starting with un-styled markup in App.js, let's look at the base structure:
import React from 'react';
function App() {
return (
<div className="">
<img className="" src={require('./profile.jpg')} alt="Display" />
<div className="">
<div className="">
Blessing Krofegha
</div>
<p className="">
When i’m not coding i switch to netflix with biscuits and cold tea as my companion. <span></span>😜
</p>
</div>
<div className="">
<span className="">#Software Engineer</span>
<span className="">#Writter</span>
<span className="">#Public Speaker</span>
</div>
</div>
);
}
export default App;
Without any classes applied, the content renders left-aligned, and the image appears at full natural size. The component contains four main div elements that will receive Tailwind classes.
Styling the Outer Container
The utility classes on the first div handle card sizing and visual depth:
import React from 'react';
function App() {
return (
<div className="max-w-sm rounded overflow-hidden shadow-lg">
<img className="w-full" src={require('./profile.jpg')} alt="Display" />
<div className="">
<div className="">
Blessing Krofegha
</div>
<p className="">
When I’m not coding, I switch to Netflix with biscuits and cold tea as my companion. <span></span>😜
</p>
</div>
<div className="">
<span className="">#Software Engineer</span>
<span className="">#Writter</span>
<span className="">#Public Speaker</span>
</div>
</div>
);
}
export default App;
Here's what each class accomplishes:
max-w-smcaps the card's width for smaller screens.roundedapplies border-radius for softer corners.overflow-hiddenprevents any scrolling bars from appearing.shadow-lguses box-shadow to add elevation — 0px from the top, 10px right, 15px bottom, and -3px left with a faint black tint on one axis, and a lighter rgba(0,0,0, 0.05) falloff at 0px top, 4px right, 6px bottom, and -2px left.
For the image inside, w-full stretches it to fill the card's width. Add your src and alt attributes as usual.
The result is your first Tailwind-powered profile card:
Filling In The Card Details
The remaining sections of the profile card rely on padding and typography utility classes. The second div gets horizontal padding of 1rem (px-6) and vertical padding of 1.5rem (py-4):
import React from 'react';
function App() {
return (
<div className="max-w-sm rounded overflow-hidden shadow-lg">
<img className="w-full" src={require('./profile.jpg')} alt="Display" />
<div className="px-6 py-4">
<div className="">
Blessing Krofegha
</div>
<p className="">
When i’m not coding i switch to netflix with biscuits and cold tea as my companion. <span></span>😜
</p>
</div>
<div className="">
<span className="">#Software Engineer</span>
<span className="">#Writter</span>
<span className="">#Public Speaker</span>
</div>
</div>
);
}
export default App;
The third div holds the title and descriptive text. The title uses font-bold for a font-weight of 700, text-purple-500 for a light purple color, and text-xl for a larger font size. mb-2 adds a bottom margin of 0.5rem. The paragraph beneath it is styled with text-gray-700, which maps to color: #4a5568, and text-base for a font size of 1rem.
import React from 'react';
function App() {
return (
<div className="max-w-sm rounded overflow-hidden shadow-lg">
<img className="w-full" src={require('./profile.jpg')} alt="Display" />
<div className="px-6 py-4">
<div className="font-bold text-purple-500 text-xl mb-2">
Blessing Krofegha
</div>
<p className="text-gray-700 text-base">
When i’m not coding i switch to netflix with biscuits and cold tea as my companion. <span></span>😜
</p>
</div>
<div className="">
<span className="">#Software Engineer</span>
<span className="">#Writter</span>
<span className="">#Public Speaker</span>
</div>
</div>
);
}
export default App;
The fourth div repeats the px-6 and py-4 padding pattern. Inside it, the span elements use inline-block so they behave as inline elements while still accepting block-level properties. Each tag is given a gray background with bg-gray-200, a fully rounded pill shape via rounded-full (which applies a border-radius of 9999px), and padding of px-3 horizontally and py-1 vertically. The text inside is small (text-sm) and colored text-gray-700, with a right margin separating each tag.
import React from 'react';
function App() {
return (
<div className="max-w-sm rounded overflow-hidden shadow-lg">
<img className="w-full" src={require('./profile.jpg')} alt="Display" />
<div className="px-6 py-4">
<div className="font-bold text-purple-500 text-xl mb-2">
Blessing Krofegha
</div>
<p className="text-gray-700 text-base">
When i’m not coding i switch to netflix with biscuits and cold tea as my companion. <span></span>😜
</p>
</div>
<div className="px-6 py-4">
<span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2">#Software Engineer</span>
<span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2">#Writter</span>
<span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mt-2 ml-20">#Public Speaker</span>
</div>
</div>
);
}
export default App;
Note: The avatar image can be swapped for any image of your choice, and the content can be personalized freely.
Trimming The Build For Production
Before shipping, the generated CSS and JavaScript files need significant size reduction. The default build produces a CSS file that is far too heavy for production use — well over 180 KB of mostly unused utility classes.
To strip out unneeded styles, install the PurgeCSS plugin for PostCSS:
npm i @fullhuman/postcss-purgecss
Then update postcss.config.js with the following configuration:
const tailwindcss = require("tailwindcss");
module.exports = {
plugins: [
tailwindcss("./tailwind.js"),
require("autoprefixer"),
require("@fullhuman/postcss-purgecss")({
content: ["./src/**/*.js", "./public/index.html"],
defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g)|| [],
}),
],
};
Here's what the configuration accomplishes:
@fullhuman/postcss-purgecssis required and invoked as a function with the provided options.- The
contentproperty lists the paths to all template files — in this case, thejsandhtmlfiles where class names appear. - The
defaultExtractorkey tells PurgeCSS how to detect class names. The function reads file contents and uses a regular expression to find class-like tokens. - The regex matches sequences containing uppercase and lowercase letters, numbers, underscores, colons, and slashes. Non-matching content returns an empty array.
Running npm run build afterward should show a dramatically smaller output:
The CSS file drops from 186.67 KB to roughly 1.02 KB — a reduction that makes the bundle production-ready. 👌



