You've been there. You build a gorgeous layout on your 1440p monitor, push it to production, and then open it on your phone. The text is microscopic, the button overflows the screen, and the spacing looks like it was designed by someone who hates mobile users.
The culprit is almost always the same: hardcoded pixel values.
This isn't about blaming beginners. Pixels feel intuitive. font-size: 16px is readable. padding: 24px makes sense visually. But pixels are absolute — they don't care about the user's browser settings, their device pixel ratio, or the fact that their monitor is 5,120px wide.
Let's fix this properly.
Why Pixels Break at Scale
Pixels (px) are fixed units. They ignore the user's font size preference in the browser, and they don't scale with the viewport unless you force them to with media queries — which is just you writing different hardcoded values for different breakpoints.
/* This breaks at unexpected viewport sizes */
.card {
font-size: 14px;
padding: 20px;
max-width: 600px;
}You end up with an infinite whack-a-mole of @media queries. 768px, 1024px, 1280px — breakpoints chosen by convention rather than by your actual content.
There's a better mental model.
The Right Units: rem and em
rem — Relative to Root
1rem equals the font size of the <html> element, which browsers default to 16px. This means:
1rem = 16px1.5rem = 24px0.875rem = 14px
Why is this better? Because if a user has bumped their browser font size to 20px (many visually impaired users do this), your entire layout scales proportionally. You get accessibility for free.
In Tailwind, rem-based values are the default:
<!-- text-base = 1rem, p-6 = 1.5rem -->
<div class="text-base p-6 max-w-prose">...</div>em — Relative to Parent
1em is relative to the current element's font size. This makes em perfect for component-level spacing — padding, margin, and borders that should scale with the component's own text size.
.badge {
font-size: 0.75rem;
padding: 0.25em 0.75em; /* scales with the badge's own font size */
border-radius: 0.5em;
}If you later increase the badge's font size, the padding expands naturally. It stays proportional without any edits.
Rule of thumb: Use rem for global layout and typography. Use em for component internals.
Fluid Typography with clamp()
This is where it gets genuinely powerful.
clamp(min, preferred, max) is a CSS function that pins a value between a minimum and maximum, while allowing it to scale fluidly between them based on viewport width.
h1 {
font-size: clamp(1.75rem, 4vw, 3.5rem);
}What this does:
- Never goes below
1.75rem(mobile) - Scales linearly with the viewport using
4vw - Caps at
3.5rem(large screens)
No media queries. No JavaScript. The heading is always readable, always proportional, always capped.
Using clamp() in Tailwind
Tailwind's default scale doesn't expose clamp() directly, but it's trivial to wire it into your config:
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontSize: {
"fluid-sm": "clamp(0.875rem, 2vw, 1rem)",
"fluid-base": "clamp(1rem, 2.5vw, 1.125rem)",
"fluid-lg": "clamp(1.125rem, 3vw, 1.5rem)",
"fluid-xl": "clamp(1.5rem, 4vw, 2.25rem)",
"fluid-2xl": "clamp(1.75rem, 5vw, 3rem)",
"fluid-3xl": "clamp(2rem, 6vw, 4rem)",
},
},
},
};Then in your JSX:
<h1 className="text-fluid-3xl font-bold tracking-tight">
Build Things That Scale
</h1>
<p className="text-fluid-base text-zinc-400 max-w-prose">
This paragraph reads perfectly at 375px and 2560px alike.
</p>Building a "Locked-In" Layout
A locked-in layout is one that looks intentional at every viewport width — not just at the breakpoints you tested.
Here's a pattern I use in almost every project:
// A section that scales its padding and font fluidly
export function Section({ children }) {
return (
<section
className="
px-[clamp(1rem,5vw,4rem)]
py-[clamp(2rem,8vw,6rem)]
max-w-[90rem]
mx-auto
"
>
{children}
</section>
);
}The [clamp(...)] syntax is Tailwind's arbitrary value support — you can drop any valid CSS into square brackets.
Container Queries (Bonus)
If you're targeting Tailwind v3.3+ or v4, container queries let you style components based on their container's width rather than the viewport. This is a game-changer for reusable components like cards:
<div class="@container">
<div class="flex flex-col @md:flex-row gap-4">
<!-- switches layout when the container hits md width, not the viewport -->
</div>
</div>The Mental Model Shift
Stop thinking in "this needs to look right at 768px." Start thinking in ranges:
- What's the minimum readable state?
- What's the maximum before it looks too spread out?
- Does everything between those two points feel intentional?
When you design with rem, em, and clamp(), the answer to all three is almost always yes — automatically.
Your UI stops being a collection of fixed snapshots at known breakpoints and becomes something genuinely fluid. That's the shift.
If you found this useful, I write about React, CSS architecture, and dev tooling. You can find everything I'm building on my portfolio.