Designing Responsive WordPress Pages with HTML and CSS
In this article, we'll explore how developers can design responsive WordPress pages using HTML and CSS, providing practical tips for both new and experienced developers.

A practical walkthrough of integrating Tailwind CSS versions 3 and 4 into React and Next.js projects, including dark mode configuration and real-world development patterns.
I remember the first time I opened a codebase using Tailwind CSS. My immediate reaction was something like "why are there so many classes on every element?" Coming from traditional CSS where you'd write separate stylesheets, it felt backwards. But after building a few components, something clicked. I stopped context-switching between files. I stopped agonizing over class names. I just built things.
This guide emerged from setting up Tailwind across dozens of projects. I'll walk you through both version 3 (which you'll encounter in most existing codebases) and version 4 (the latest release that changes quite a bit under the hood). We'll cover React with Vite and Next.js setups, tackle dark mode implementation properly, and address the gotchas I wish someone had explained to me earlier.
Traditional CSS follows a familiar pattern: write HTML, jump to a CSS file, create class names, style them, jump back to HTML, realize you need another class, repeat. Tailwind's utility-first approach flips this entirely. You compose styles directly in your markup using pre-built classes that each do one specific thing.
Want some padding? Add
p-4. Need different padding on larger screens? Make it
md:p-8. Want to change the text color? Use
text-gray-700. At first glance, this looks messy. You'll write components with long lists of classes, and your inner voice will scream about separation of concerns. Push through that discomfort for a few hours. You'll discover you're building interfaces significantly faster because you're staying in one mental context.
The clever part happens during your build process. Tailwind scans your entire codebase looking for class names, then generates CSS containing only what you actually used. Despite having thousands of available utilities in Tailwind's design system, your production bundle stays remarkably small.
Most React projects these days use Vite rather than Create React App, and for good reason. Vite's development server starts instantly and hot reloading actually feels hot. Let's set up Tailwind v3 in a fresh Vite project.
Start by creating your React project. I typically use TypeScript because it catches so many silly mistakes before runtime:
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
Now we'll install Tailwind version 3 along with its dependencies. Tailwind v3 relies on PostCSS and Autoprefixer to transform and optimize your CSS during the build process:
npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
That initialization command creates two configuration files for you. The
-pflag tells it to generate a PostCSS config alongside the Tailwind config, which saves you from creating one manually.
Open the newly created
tailwind.config.jsfile. This is where you tell Tailwind where to look for utility classes in your codebase. The configuration uses a content array that defines which files Tailwind should scan:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
// This is where you'd add custom colors, fonts, spacing, etc.
// We'll leave it empty for now
},
},
plugins: [],
}
The content paths are absolutely critical. If you add components in a directory that's not covered by these glob patterns, Tailwind won't find your classes and your styles will mysteriously not work. I've spent embarrassing amounts of time debugging styling issues only to realize I forgot to update these paths.
Next, replace everything in
src/index.csswith Tailwind's three essential directives:
@tailwind base;
@tailwind components;
@tailwind utilities;
These Tailwind directives inject different layers of styles into your CSS. The base layer provides sensible defaults and resets. The components layer is where you'd define reusable component classes (though we'll rarely use this). The utilities layer contains all those handy classes like
flex,
pt-4, and
text-blue-500.
Make sure you're importing this CSS file in your
src/main.tsx:
import './index.css'
Start your development server with
npm run devand you're ready to start styling components with Tailwind utilities.
Tailwind v4 launched in late 2024 with a fundamental architectural change. The team rebuilt the engine in Rust, which makes everything faster. More importantly for setup, they eliminated the PostCSS dependency for most use cases. This simplifies configuration considerably.
Start with the same Vite scaffolding:
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
The installation step is noticeably simpler now:
npm install tailwindcss @tailwindcss/vite
Instead of PostCSS configuration, you integrate Tailwind directly into your Vite setup. Update your
vite.config.tsfile:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// The Tailwind plugin hooks directly into Vite's build process
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
Your
src/index.cssfile becomes even simpler:
@import "tailwindcss";
That's genuinely it. No configuration file needed. The new engine automatically discovers your content files, scans them, and generates your CSS. During development, the Rust-based engine rebuilds your styles fast enough that you won't notice any delay when saving files.
Next.js projects have their own quirks, especially with the App Router that shipped in Next.js 13. The setup process differs slightly because of how Next.js structures its directories and handles CSS.
Create your Next.js project with TypeScript and the App Router:
npx create-next-app@latest my-next-app --typescript --app
cd my-next-app
Install the Tailwind dependencies:
npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
Your
tailwind.config.jsneeds to account for Next.js's file structure. The App Router uses an
appdirectory while older projects use
pages. I typically include both to ensure compatibility:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}", // for legacy pages directory
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
}
Next.js projects come with a
app/globals.cssfile. Replace its contents with Tailwind's directives:
@tailwind base;
@tailwind components;
@tailwind utilities;
This global stylesheet needs to be imported in your root layout at
app/layout.tsx:
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Version 4 works differently in Next.js compared to Vite projects. Since Next.js already uses PostCSS internally, you'll use Tailwind's PostCSS plugin rather than a bundler-specific integration.
Create your Next.js project:
npx create-next-app@latest my-next-app --typescript --app
cd my-next-app
Install Tailwind v4 with its PostCSS plugin:
npm install tailwindcss @tailwindcss/postcss
Create a
postcss.config.mjsfile in your project root:
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Update your
app/globals.cssfile:
@import "tailwindcss";
The import in your root layout stays the same. The new Tailwind engine automatically discovers your Next.js project structure and finds all your component files without explicit configuration.
Once your setup is complete, writing components feels the same whether you're using v3 or v4. Here's a card component that demonstrates common patterns you'll use constantly:
function Card({ title, description, highlighted = false }) {
return (
<div className={`
max-w-sm rounded-lg overflow-hidden shadow-lg p-6
${highlighted ? 'bg-blue-50 border-2 border-blue-500' : 'bg-white'}
hover:shadow-xl transition-shadow duration-300
`}>
<h2 className="font-bold text-xl mb-2 text-gray-900">
{title}
</h2>
<p className="text-gray-700 text-base leading-relaxed">
{description}
</p>
</div>
);
}
This component shows how to handle conditional styling using template literals. When
highlightedis true, we swap in a blue background and border. Otherwise, we get a plain white card. The hover effect and transition work regardless of the highlighted state because they're always present in the className string.
Dark mode implementation trips up many developers, myself included when I first tried it. The documentation exists but doesn't always explain why you're doing certain steps. Let me walk through both versions clearly.
First, you need to enable class-based dark mode in your
tailwind.config.js:
module.exports = {
darkMode: 'class', // This tells Tailwind to look for a 'dark' class
// ... rest of your config
}
Now create a theme toggle component. This component needs to manage adding and removing the
darkclass from your HTML element, persist the user's preference, and respect their system preferences if they haven't chosen explicitly:
// components/ThemeToggle.tsx
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState('light');
useEffect(() => {
// First, check if the user has a saved preference
const savedTheme = localStorage.getItem('theme');
// If not, check their system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Use saved preference, fall back to system preference, default to light
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
setTheme(initialTheme);
// Apply the theme by adding/removing the class on the root element
if (initialTheme === 'dark') {
document.documentElement.classList.add('dark');
}
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
// Update the DOM immediately so users see the change
if (newTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Save their preference so it persists across sessions
localStorage.setItem('theme', newTheme);
};
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700
text-gray-900 dark:text-gray-100
hover:bg-gray-300 dark:hover:bg-gray-600
transition-colors"
aria-label="Toggle theme"
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
There's one annoying problem with this approach. When someone visits your site with dark mode enabled, there's a brief flash where the page loads in light mode before React hydrates and applies the dark class. This happens because the browser renders HTML before JavaScript executes.
The solution is adding a small script that runs before React loads. In Next.js, create a component that injects this script:
// app/ThemeScript.tsx
export function ThemeScript() {
// This script runs synchronously before the page renders
const themeScript = `
(function() {
const theme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Apply dark mode immediately if needed
if (theme === 'dark' || (!theme && prefersDark)) {
document.documentElement.classList.add('dark');
}
})();
`;
return (
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
);
}
Include this in your root layout, placing it in the head section so it executes before the body renders:
// app/layout.tsx
import { ThemeScript } from './ThemeScript';
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<ThemeScript />
</head>
<body>{children}</body>
</html>
);
}
Tailwind v4 handles dark mode differently. Instead of a configuration option, you define the behavior using CSS variants. Add this to your global CSS file after importing Tailwind:
@import "tailwindcss";
/* This variant tells Tailwind to apply dark: classes when a parent has the dark class */
@variant dark (&:where(.dark, .dark *));
The theme toggle component works identically to v3. The difference is purely in how Tailwind's engine processes the dark variant during build time.
For a more sophisticated approach that works in both versions, you can use CSS custom properties. This method gives you semantic color names that automatically adapt to the current theme:
@import "tailwindcss";
:root {
--color-background: 255 255 255; /* white in RGB */
--color-text: 17 24 39; /* gray-900 */
--color-primary: 59 130 246; /* blue-500 */
}
.dark {
--color-background: 17 24 39; /* gray-900 */
--color-text: 243 244 246; /* gray-100 */
--color-primary: 96 165 250; /* blue-400, lighter for dark backgrounds */
}
Then extend your Tailwind config to use these custom properties:
module.exports = {
theme: {
extend: {
colors: {
background: 'rgb(var(--color-background) / <alpha-value>)',
text: 'rgb(var(--color-text) / <alpha-value>)',
primary: 'rgb(var(--color-primary) / <alpha-value>)',
},
},
},
}
Now you can write
bg-background,
text-text, and
text-primarythroughout your components, and they'll automatically adjust when someone toggles dark mode. This approach reduces the number of
dark:variants you need to write.
After building many projects with Tailwind, certain patterns consistently prove their worth while others create more problems than they solve.
Keep your Tailwind configuration minimal. Only extend the theme when you need consistent custom values used across multiple components. For one-off situations, arbitrary values like
bg-[#1da1f2]work perfectly fine. I've seen configurations balloon to hundreds of lines of custom utilities that nobody remembers exist.
While Tailwind encourages utility classes everywhere, don't feel guilty about creating component classes for truly repeated patterns. Use the @apply directive sparingly, though. If you find yourself using it constantly, you might be fighting Tailwind's philosophy:
@layer components {
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded-lg
hover:bg-blue-600 transition-colors;
}
}
Install the Prettier plugin for Tailwind. It automatically sorts your utility classes in a consistent order, which makes code reviews significantly easier. Without it, everyone orders classes differently and git diffs become noisy.
The official Tailwind CSS IntelliSense extension for VS Code is genuinely essential. It provides autocomplete for class names, hover previews showing the actual CSS, and warnings when you typo a class name. This extension accelerates learning because you can discover utilities without constantly checking documentation.
Remember that Tailwind makes building inaccessible interfaces easy if you're not careful. The utility classes style visual appearance, but they don't add semantic meaning or proper ARIA attributes. Always use semantic HTML elements and test your interfaces with keyboard navigation and screen readers.
When styles aren't working, I follow this checklist in order because it catches probably 95% of issues:
First, verify your content paths in the configuration file match your actual project structure. If you created a
libfolder but didn't add it to the content array, none of your components in that folder will have working styles.
Second, confirm you're importing the CSS file in your application's entry point. In React, that's typically
main.tsx. In Next.js, it's your root layout.
Third, check for typos in class names. The IntelliSense extension helps enormously here because it underlines invalid class names.
Fourth, for v3 specifically, make sure PostCSS is properly configured and the plugins are installed.
Fifth, clear your build cache and restart the development server. Sometimes the build process gets confused, especially when you've changed configuration files.
One particularly frustrating issue happens when styles work in development but break in production builds. This usually means you're dynamically constructing class names. Tailwind's build process scans your files for complete class name strings. If you write something like
text-${color}-500, Tailwind can't detect what specific classes to include. Either use complete class names or add them to the safelist in your config.
The official Tailwind CSS documentation remains your best resource for understanding individual utilities and configuration options. The docs are genuinely well-written and include searchable examples.
For Next.js-specific guidance, the Next.js styling documentation covers integration details and common patterns.
Tailwind UI is a commercial component library from the Tailwind team. Even if you don't purchase it, studying the free examples teaches you solid patterns for component architecture and responsive design.
Stay current with v4 developments by following the Tailwind Labs GitHub repository and reading their official blog. The team ships regular updates and writes excellent explanatory posts about new features.
Tailwind fundamentally changed how I build user interfaces. Whether you stick with the stable version 3 or adopt version 4's improvements, you're working with a tool that genuinely scales from quick prototypes to large production applications.
Success with Tailwind isn't about memorizing every utility class. That's impossible and unnecessary. Instead, internalize the mental model of composing designs from small, purposeful pieces. Start with basic layout utilities like flexbox and spacing. Gradually explore responsive design, hover states, and transitions. When you see a website you admire, inspect it and learn from their approaches.
Remember that Tailwind is a tool, not a dogma. Combine it with other approaches when they serve your project better. Use CSS modules for complex animations. Write custom CSS when you need precise control. Your goal is shipping excellent user experiences, and Tailwind is simply one powerful way to get there more quickly.
In this article, we'll explore how developers can design responsive WordPress pages using HTML and CSS, providing practical tips for both new and experienced developers.
This guide covers essential topics that we need to know to make high-quality designs **that are **consistent with Apple’s guidelines.
Why did we bother building all these fancy web interfaces, when all we ever needed was a text box?