The article started teaching Tailwind workshops in 2023, and It has watched hundreds of developers make the same handful of mistakes. They're not career-ending mistakes, but they slow you down, cause weird bugs, and frustrate the people reviewing your PRs. After running about a dozen workshops and reviewing thousands of pull requests from junior engineers, It has narrowed it down to the five mistakes that come up most often. If you're new to Tailwind — or you're reviewing PRs from someone who's new — these are the patterns to watch for. Some of them hide real performance or accessibility bugs that only show up in production.
Why This Matters
Tailwind has a learning curve that's steeper than it looks. The first day feels like a productivity win; the first month reveals a series of "wait, why isn't this working?" moments. Most of those moments trace back to one of these five mistakes. Learning what they are upfront saves you dozens of hours of debugging later — and in production codebases, those debugging hours compound. Documentation and common practice have teams spend entire sprints chasing bugs that trace back to a single repeated mistake.
There's also a code review dimension. When everyone on the team understands these pitfalls, PRs get reviewed faster and the codebase stays consistent. When nobody does, you accumulate a layer of small inconsistencies that compound into real maintenance pain. A codebase with 200 small variations of "almost the same button" is a codebase that takes twice as long to modify.
Finally, these mistakes often hide real performance or accessibility issues. The "looks fine in dev but breaks in production" pattern frequently traces back to one of these. h-screen looks identical to min-h-screen on desktop but breaks on iOS Safari. Arbitrary values look identical to tokens in code review but balloon your HTML and break theming. Catching them early means shipping cleaner, faster apps — and avoiding the embarrassment of having accessibility issues flagged in QA.
The Core Idea
The five mistakes It is about to describe share a common theme: they're all cases where a developer reaches for a familiar CSS mental model instead of the Tailwind way of doing things. Tailwind's API is well-designed, but it requires you to unlearn some CSS habits. CSS habits from 2015 — the era of hand-written stylesheets and BEM naming — often lead you astray in a utility-first world.
The mistakes are:
- Using arbitrary values when a token exists. This is the most common and most damaging mistake.
- Hardcoding responsive breakpoints that don't match the design. Causes inconsistent scaling and surprises at certain viewport widths.
- Forgetting about cascade and specificity. Creates order-dependent bugs that are hard to debug.
- Using
h-screeninstead ofmin-h-screen. Breaks on mobile browsers in ways that don't show up in dev. - Building custom CSS files when utilities would do. Reinvents the wheel and adds maintenance burden.
Each of these has a "why it's wrong" and a "what to do instead." The following walks through them in detail with code, fix patterns, and the underlying mental model that prevents the mistake from happening in the first place.
A Concrete Example
Shown below each mistake with a real code snippet and the corrected version.
Mistake 1: Arbitrary values when a token exists.
This is the most common mistake. A junior dev needs a brand color and writes:
// Bad — arbitrary value, not themable, not discoverable
<button className="bg-[#3b82f6] text-[#ffffff]">Click me</button>
The correct version uses a token:
// Good — uses theme tokens, themable, discoverable
<button className="bg-brand-500 text-white">Click me</button>
The arbitrary value version has three problems: it's not themeable (changing the brand color requires searching for hex codes), it's not discoverable (you can't see what colors are available without reading the markup), and it bloats your HTML (those long hex strings add up). When your designer decides to update the brand color to a slightly different blue, you'll be hunting through your codebase for every bg-[#...] instead of changing one token.
The rule of thumb: if you find yourself writing bg-[#...] or text-[#...], you need a new token in your theme. Add --color-brand-500: #3b82f6 to @theme and use bg-brand-500 instead. The token pays for itself the first time someone asks "what's our brand color?"
Mistake 2: Hardcoded responsive breakpoints that don't match the design.
A junior dev sees sm:, md:, lg: and assumes they should pick from those breakpoints:
// Bad — uses arbitrary breakpoint that doesn't match design
<div className="text-base mid:[28rem]:text-lg">
This text scales at a weird breakpoint
</div>
The correct version uses the design's actual breakpoints, which usually map to Tailwind's defaults:
// Good — uses Tailwind's standard breakpoints
<div className="text-base md:text-lg">
This text scales at a sensible breakpoint
</div>
Tailwind's default breakpoints are well-chosen: sm (640px), md (768px), lg (1024px), xl (1280px), 2xl (1536px). They match the breakpoints most designers use. Custom breakpoints should be reserved for genuinely unusual layouts — say, a kiosk application that runs at specific resolutions. If you find yourself adding custom breakpoints, double-check whether the design actually demands them or whether you're creating inconsistency.
If you need custom breakpoints, declare them in @theme:
@theme {
--breakpoint-3xl: 1920px;
--breakpoint-tablet: 768px;
}
Then use them as 3xl:text-lg or tablet:text-base.
Mistake 3: Cascade and specificity issues.
A junior dev writes a button component, then realizes they need a "secondary" variant:
// Bad — relies on cascade, breaks when classes appear in different order
export function Button({ children, variant }) {
return (
<button className={`
bg-blue-500 text-white
${variant === 'secondary' ? 'bg-white text-gray-900' : ''}
`}>
{children}
</button>
);
}
The problem: when both classes apply, the later one wins, but only if it's later in the stylesheet — not in the markup. With Tailwind's source order, bg-blue-500 might come after bg-white, breaking the variant. This kind of bug is incredibly frustrating to debug because the markup looks correct, the styles look correct, but the result is wrong.
The fix is to use explicit, mutually exclusive styles:
// Good — explicit, predictable, order-independent
export function Button({ children, variant }) {
const styles = variant === 'secondary'
? 'bg-white text-gray-900 ring-1 ring-gray-300'
: 'bg-blue-500 text-white hover:bg-blue-600';
return <button className={styles}>{children}</button>;
}
Better yet, use a variant lookup table:
const variants = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-white text-gray-900 ring-1 ring-gray-300 hover:bg-gray-50',
ghost: 'text-gray-700 hover:bg-gray-100',
};
export function Button({ children, variant = 'primary' }) {
return (
<button className={`
inline-flex items-center px-4 py-2 rounded-md font-semibold
transition-colors
${variants[variant]}
`}>
{children}
</button>
);
}
The lookup table pattern makes variants explicit, prevents accidental overlap, and gives you a single place to add new variants. It's the pattern every production design system uses.
Mistake 4: h-screen instead of min-h-screen.
A junior dev wants a full-height landing page section:
// Bad — h-screen is exactly 100vh, which causes issues on mobile
<section className="h-screen flex items-center">
<h1>Welcome to our site</h1>
</section>
The problem: 100vh on mobile browsers includes the area behind the URL bar, which means content gets cut off. The fix is min-h-screen, which lets content grow beyond the viewport:
// Good — content can grow beyond the viewport
<section className="min-h-screen flex items-center">
<h1>Welcome to our site</h1>
</section>
In Tailwind v4, there's also a new h-dvh utility that uses the dynamic viewport height (the actual visible area). For full-screen landing sections that adapt to mobile browser UI, h-dvh is even better:
// Better — handles mobile browser UI gracefully
<section className="min-h-dvh flex items-center">
<h1>Welcome to our site</h1>
</section>
dvh (dynamic viewport height) accounts for the changing visible area when mobile users scroll. As the URL bar slides out of view, the dvh value updates. This is the kind of subtle detail that distinguishes a "looks fine in dev" app from one that actually works on real phones.
Mistake 5: Building custom CSS when utilities would do.
A junior dev needs a flexbox centering pattern and writes:
/* Bad — custom CSS for something Tailwind already does */
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
<div className="flex-center">
<p>Centered content</p>
</div>
The correct version uses Tailwind's built-in utilities:
// Good — uses utilities, no custom CSS needed
<div className="flex items-center justify-center">
<p>Centered content</p>
</div>
The custom CSS version has three problems: it requires a CSS file, it adds to the cascade, and it makes the markup less self-documenting. The utilities version is self-explanatory — anyone reading flex items-center justify-center knows exactly what's happening without context switching to a CSS file.
If you find yourself writing custom CSS for a common pattern, search Tailwind's docs first. There's almost certainly a built-in. The framework has hundreds of utilities covering the vast majority of CSS patterns you'll need.
Common Pitfalls Beyond the Top 5
Once you've fixed the top five, watch for these:
Reaching for
space-x-*instead offlex gap-*.space-xadds margins to all but the first child;gapworks for any flex/grid layout and composes better. With conditional rendering,space-x-*can produce visual gaps that don't make sense.Forgetting
focus-visible:styles. Tailwind's default focus styles are subtle; many beginners strip them. Always include a focus state — it's an accessibility requirement, not a nice-to-have.Using
text-smandfont-boldseparately. Tailwind has combined classes liketext-sm font-semiboldthat work well together. The defaults for each text size are designed to pair with specific weights.Ignoring the container query variants.
@md:text-lgis more useful thanmd:text-lgfor component-level responsive design. When a component is reused inside different parent widths, container queries are more accurate than viewport queries.Reaching for
flex-shrink-0instead ofshrink-0. Tailwind has shorter aliases for common properties. The full formflex-shrink-0works butshrink-0is the modern style.Not using the
size-*utilities. Setting equal width and height withw-10 h-10is verbose;size-10does the same thing.Hardcoding colors instead of using semantic tokens. Use
bg-successorbg-danger(custom tokens) instead ofbg-green-500orbg-red-500when the meaning matters more than the appearance.
When to Use This (And When Not To)
This guide is for beginners, but the principles apply to experienced developers too. If you're reviewing PRs from junior devs, look for these patterns and gently redirect. If you're a junior dev reading this, share it with your team — it will save everyone time.
If you're already past these mistakes, consider running a workshop for new hires. Teaching these concepts is a great way to internalize them yourself; explaining a concept is the best way to truly understand it. The mistakes It has listed here aren't just "things beginners do" — they're concepts that reward deeper understanding.
There are also legitimate cases where these "mistakes" are correct. If you have a genuinely one-off color that will never be reused, an arbitrary value is fine. If you're building a kiosk app for a specific screen size, custom breakpoints are fine. The principle is: know when you're breaking the pattern and do it intentionally.
Wrapping Up
The five mistakes above are responsible for most of the "Tailwind is annoying" complaints Reported. None of them are inherent to Tailwind — they're habits imported from other styling approaches. Unlearn those habits, learn the Tailwind way, and the framework becomes dramatically more pleasant to work with. Pick one of these five mistakes and audit your current codebase — you'll likely find a few cases to fix today. Start with arbitrary values, since those have the highest cleanup cost and the largest impact on maintainability.
A Self-Audit Exercise
The remainder gives you a concrete exercise you can do today. Open your main component file and grep for each of these patterns:
# Find arbitrary values
grep -rn '\[#' src/
# Find hardcoded breakpoints (look for unusual prefixes)
grep -rn '\b[a-z][a-z]*:\[' src/
# Find h-screen usage
grep -rn 'h-screen' src/
# Find custom CSS files
find src/ -name "*.css" -not -name "*.module.css"
For each match, ask: "Is this necessary, or could Used a built-in utility?" You'll be surprised how many cases there are.
A Quick-Reference Table
Here's a quick reference for the right Tailwind way to do common things beginners get wrong:
| Instead of... | Use... |
|---|---|
bg-[#3b82f6] |
bg-blue-500 (or your brand token) |
mid:[28rem]:text-lg |
md:text-lg (default breakpoints) |
h-screen for full page |
min-h-screen or min-h-dvh |
Custom .flex-center class |
flex items-center justify-center |
space-x-4 with conditional children |
flex gap-4 |
w-4 h-4 for square sizing |
size-4 |
text-blue-500 font-bold |
text-blue-500 font-semibold (semibold is usually what you want) |
This table should cover 80% of the cases you'll see. For the remaining 20%, the official Tailwind docs are excellent.
When to Break These Rules
Like any rule, these can be broken intentionally. If you have a one-off color that's genuinely unique to your brand, an arbitrary value is fine. If you're building a kiosk app at a specific resolution, custom breakpoints are fine. The principle: know when you're breaking the pattern and do it intentionally.
A good rule of thumb: if you find yourself reaching for a workaround more than three times, the workaround is probably the right answer for your situation. Update your team conventions accordingly.
The Story Behind Mistake #4
Shared below a war story about h-screen because it cost the team a real production bug. We built a beautiful landing page in early 2025 with a hero section that used h-screen to fill the viewport. Looked gorgeous in Chrome DevTools. Looked gorgeous in the iOS Simulator. Looked gorgeous on every tester's Android device. Shipped it on a Friday.
By Monday morning we had eleven support tickets from iPhone users saying the bottom of the hero section was getting cut off. The URL bar was overlapping our content. After some debugging we realized that on iOS Safari, 100vh includes the area behind the URL bar — but when the user scrolls, the URL bar slides away and that area becomes visible. So our hero section was the wrong height depending on whether the user had scrolled.
The fix was a one-character change to min-h-screen. The lesson was bigger: viewport units on mobile are deceptive. The newer dvh (dynamic viewport height) unit exists exactly because this is a known platform quirk.
If you're building anything that needs to fill the viewport on mobile, use min-h-dvh. Test it on a real phone, not just DevTools. The DevTools mobile emulator doesn't accurately simulate iOS Safari's URL bar behavior.
The Real Cost of Arbitrary Values
Recommended to expand on Mistake #1 because it's the one with the longest tail. The cost of bg-[#3b82f6] is invisible the day you write it. It works. It looks right. You ship it. Then six months later your designer says "we're tweaking the brand color slightly" and you spend three days hunting through 200+ files for hex codes that need to change.
Documentation and common practice have this exact scenario play out multiple times. The first time, the team spent a full sprint updating the brand color across their codebase. They estimated the change at "an hour" and turned it into two weeks. After that, they instituted a rule: no arbitrary values for colors. Every color must be a token.
Tokens aren't just about refactoring convenience. They're about communication. When a designer asks "what's the brand color?" you should be able to say "Below is a demonstration of the CSS file" and point at --color-brand-500. When a new engineer joins and wonders what colors are available, they should be able to read the @theme block and see the palette. When you want to swap brand palettes for a white-label product, you swap the theme.
Arbitrary values defeat all of this. They're a one-time convenience that becomes a long-term tax. The token system isn't more work — it's just front-loading the work in a way that pays back forever.
The same logic applies to spacing, typography, and breakpoints. If you find yourself reaching for arbitrary values repeatedly, you probably need to formalize a token. The rule: arbitrary values are for genuinely unique one-offs. If you use it twice, it's a token.
How to Teach These to Your Team
If you're a senior engineer or tech lead, the most impactful thing you can do with this list is teach it. Don't just fix the patterns — explain WHY they're wrong, in PR comments, in team Slack, in onboarding docs.
Documentation and common practice have a few teaching patterns that work well:
Create a "Tailwind conventions" doc in your team wiki. List the patterns you want to encourage and discourage. Include code examples. Link to the official docs.
Add a checklist to your PR template. "Did you use a token instead of an arbitrary value? Did you use min-h-screen instead of h-screen?" PR templates are passive but effective.
Run a quarterly audit. Spend an afternoon grepping the codebase for the patterns in this article. Fix the worst offenders. Track the trend over time.
Pair on the first few PRs from new hires. Don't just review — sit with them and explain. Five minutes of explanation saves dozens of "wait, why?" moments later.
Celebrate clean code. When someone writes a particularly clean Tailwind snippet, call it out. Positive reinforcement beats negative feedback.
The team Many teams worked with most recently adopted these patterns and saw a measurable improvement in PR review time. Engineers stopped getting blocked on "wait, what does this class do?" questions because the conventions were clear. The codebase got noticeably cleaner over six months. None of it was technical — it was all cultural.
Common Confusion About Tailwind's Mental Model
One last thing worth mentioning: beginners often think Tailwind's mental model is "the same as CSS, just faster." It's not. The mental model is "composable visual primitives, applied at the markup layer." That's a different thing.
In CSS, you write rules that target selectors. In Tailwind, you compose utility classes that target individual elements. The cascade still exists, but you're not really relying on it. Specificity still exists, but Tailwind handles most of it for you with utility order. Naming conventions still exist (for @apply rules and custom utilities), but you don't need BEM.
When you fully internalize this mental model, Tailwind becomes dramatically more pleasant. You stop fighting it. You start composing. You realize that bg-blue-500 text-white px-4 py-2 rounded isn't a mess — it's a complete description of a button's visual state in a way that's readable and maintainable.
The mistake isn't "using Tailwind wrong." The mistake is bringing CSS mental models that don't apply and getting frustrated when Tailwind doesn't behave the way you expect. Once you let go of those models and embrace the utility-first approach, everything clicks.
Further Reading
- Tailwind CSS best practices
- Common Tailwind mistakes (community discussion)
- [The utility-first mindset](https://adamwathan.the team/css-utility-classes-and-separation-of-concerns/)
- Tailwind v4 migration guide
- Refactoring UI book
- Mobile viewport height quirks explained
Hermes Smith
