
How to Write Mobile-First CSS with Media Queries
Most stylesheets I inherit on client projects were written desktop-first. The base styles describe a three-column layout, and then a pile of max-width media queries tries to undo it all for smaller screens: floats removed, widths reset, sidebars hidden, font sizes shrunk. It works, but every mobile rule is a correction of something that shouldn't have applied in the first place. Mobile-first CSS flips that around. You write the simplest layout first, which is almost always the small-screen one, and then add complexity as the viewport grows.
This guide explains why that approach produces cleaner code, how to structure your media queries, how to pick breakpoints, and how to build a real page layout step by step.
What Mobile-First Actually Means
Mobile-first is a way of ordering your CSS, not a design philosophy about phones. The rule is simple:
- Base styles (outside any media query) target the smallest screens.
- Media queries use
min-width(or the range syntaxwidth >= ...) to layer on changes for larger screens.
/* Base: every screen gets this */
.product-list {
display: grid;
gap: 1rem;
}
/* Wider screens add columns */
@media (min-width: 40rem) {
.product-list {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 64rem) {
.product-list {
grid-template-columns: repeat(4, 1fr);
}
}
The desktop-first equivalent starts with four columns and uses max-width queries to take them away. Both give the same result in the browser, but the mobile-first version is shorter and every rule moves in one direction: more.
Why Mobile-First Produces Better CSS
Small-Screen Layouts Are Simpler
On a narrow screen, most content just stacks in a single column. That's what block elements do by default, so your base styles barely need any layout code. Multi-column layouts, sticky sidebars, and hover effects are additions. Adding things is easier to reason about than removing them.
Fewer Overrides
In desktop-first CSS, a phone downloads and parses the desktop rules, then parses more rules to cancel them. Every override adds specificity pressure and another place for bugs. Mobile-first stylesheets tend to have far fewer "reset" declarations like float: none, width: auto, and display: block.
Older and Unusual Browsers Get a Working Layout
If a browser or reader mode ignores your media queries, it falls back to the base styles. With mobile-first, that's a readable single-column page. With desktop-first, it's a squashed multi-column layout.
It Forces Content Priorities
When you design for a small screen first, you have to decide what matters. That discipline tends to make the large-screen version better too.
Step 1: Set Up the Viewport
None of this works without the viewport meta tag. Without it, mobile browsers pretend to be about 980px wide and zoom out, so your min-width queries fire when they shouldn't.
<meta name="viewport" content="width=device-width, initial-scale=1" />
Don't add maximum-scale=1 or user-scalable=no. They block pinch zoom, which is an accessibility failure.
Step 2: Write the Base Styles
Start with a small screen in your browser's device toolbar (360px wide is a good default) and write the styles for it. Focus on typography, spacing, and colors. Layout is mostly just vertical stacking.
<body>
<header class="site-header">
<a class="logo" href="/">Harbor Coffee</a>
<nav class="site-nav" aria-label="Main">
<ul>
<li><a href="/menu">Menu</a></li>
<li><a href="/locations">Locations</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main class="layout">
<article class="content">
<h1>Seasonal Roasts</h1>
<p>Our autumn lineup, roasted weekly in small batches.</p>
</article>
<aside class="sidebar">
<h2>Visit Us</h2>
<p>Open daily, 7am to 6pm.</p>
</aside>
</main>
</body>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
font-size: 1rem;
line-height: 1.6;
color: #1e293b;
}
img {
max-width: 100%;
height: auto;
}
.site-header {
padding: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.site-nav ul {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1rem;
margin: 0.5rem 0 0;
padding: 0;
list-style: none;
}
.site-nav a {
display: inline-block;
padding: 0.5rem 0; /* comfortable touch target */
}
.layout {
display: grid;
gap: 2rem;
padding: 1rem;
}
h1 {
font-size: 1.75rem;
line-height: 1.2;
}
Notice there's nothing phone-specific here. It's just sensible defaults that happen to work in a narrow column.
Step 3: Find Your Breakpoints from the Content
The most common mistake is picking breakpoints from device lists: 375px for this phone, 768px for that tablet. Devices change every year. Instead, widen the browser slowly until the design starts to look wrong, then add a breakpoint there.
Signs that you need a breakpoint:
- Lines of text get longer than about 75 characters.
- Cards stretch so wide they look empty.
- There's enough room for a sidebar beside the content.
- The navigation has plenty of space to sit on one line next to the logo.
Most sites end up with two or three major breakpoints and the occasional component-specific tweak. Here's a reasonable starting set:
/* ~640px: large phones in landscape, small tablets */
@media (min-width: 40rem) {
}
/* ~1024px: tablets in landscape, small laptops */
@media (min-width: 64rem) {
}
/* ~1280px: desktops */
@media (min-width: 80rem) {
}
Why rem Instead of px?
Media queries written in rem or em respond to the user's default font size. If someone sets their browser text size to 20px instead of 16px, a 40rem breakpoint triggers at 800px instead of 640px, so their larger text gets the roomier single-column layout for longer. Pixel breakpoints ignore that preference.
One quirk to know: inside media queries, rem and em are both based on the browser's initial font size, not the one you set on html. So 40rem in a media query is always relative to the user's preference, even if your CSS sets html { font-size: 62.5% }.
Step 4: Layer On Larger-Screen Styles
Now widen the viewport and add rules as the content asks for them.
@media (min-width: 40rem) {
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-inline: 2rem;
}
.site-nav ul {
margin: 0;
gap: 1.5rem;
}
h1 {
font-size: 2.25rem;
}
}
@media (min-width: 64rem) {
.layout {
grid-template-columns: minmax(0, 1fr) 18rem;
max-width: 72rem;
margin-inline: auto;
padding: 2rem;
}
.sidebar {
position: sticky;
top: 2rem;
align-self: start;
}
}
The sidebar is just a stacked block on small screens and becomes a sticky column once there's room. We never had to "undo" anything.
Step 5: Reduce the Number of Breakpoints You Need
Modern CSS lets many components adapt on their own, without any media queries. Use these before reaching for another breakpoint.
Intrinsic Grids
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1.5rem;
}
That single rule gives you one column on phones and as many 16rem-plus columns as fit on larger screens. The min(100%, 16rem) stops the grid from overflowing on screens narrower than 16rem.
Fluid Type and Spacing
h1 {
font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem);
}
.section {
padding-block: clamp(2rem, 5vw, 5rem);
}
Fluid values scale smoothly between a minimum and maximum, so you don't need a breakpoint for every font size change. Keep a rem component in the middle value so the text still responds to browser zoom.
Flex Wrapping
.button-row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.button-row > * {
flex: 1 1 12rem;
}
Buttons sit side by side when there's room and stack when there isn't.
Container Queries for Components
Media queries look at the viewport. When a component can appear in both a wide main column and a narrow sidebar, the viewport tells you nothing useful. That's the job of container queries, which are supported in all current major browsers:
.card-wrapper {
container-type: inline-size;
}
.card {
display: grid;
gap: 1rem;
}
@container (min-width: 30rem) {
.card {
grid-template-columns: 10rem 1fr;
}
}
The mobile-first principle is the same: base styles for the narrow case, min-width to enhance. Use media queries for page-level layout and container queries for reusable components.
Handling Hover and Touch
Screen width and input type are separate things. A large tablet is wide but has no hover, and a small laptop window is narrow but has a mouse. Don't tie hover effects to width. Use interaction media features instead:
.card {
transition: transform 0.2s ease;
}
@media (hover: hover) and (pointer: fine) {
.card:hover {
transform: translateY(-4px);
}
}
Touch users won't get a "sticky" hover state after tapping, and mouse users get the effect whatever their window size.
Organizing Mobile-First Stylesheets
There are two common ways to organize the queries.
Grouped by breakpoint: all base styles first, then one big @media (min-width: 40rem) block, then one for 64rem. It's easy to see everything that changes at a given size, but a component's styles end up scattered across the file.
Grouped by component: each component's base styles are followed immediately by its own media queries. This keeps related code together, and it's what I recommend for most projects, especially with native CSS nesting, which works in all current browsers:
.pricing-table {
display: grid;
gap: 1rem;
@media (min-width: 48rem) {
grid-template-columns: repeat(3, 1fr);
}
}
Repeating the same media query in many places has no meaningful performance cost. Browsers handle it fine, and gzip compresses the repetition away.
Common Pitfalls
- Mixing min-width and max-width freely. An occasional
max-widthquery for a small-screen-only tweak is fine, but if half your queries go each direction, the cascade gets hard to follow. - Overlapping boundaries. If you do combine them,
max-width: 40remandmin-width: 40remboth match at exactly 40rem. The range syntax (width < 40remandwidth >= 40rem) avoids that overlap cleanly. - Hiding content on mobile.
display: noneon small screens usually means the content wasn't important, or it deserves a better small-screen treatment. Mobile users want the full information too. - Tiny touch targets. Aim for at least 44×44px interactive areas on touch devices. Padding on links and buttons is the easiest fix.
- Testing only in DevTools. Device emulation is great for layout, but test on a real phone for touch, performance, and on-screen keyboard behavior.
Conclusion
Mobile-first CSS comes down to one habit: write the simplest version first and use min-width queries to add to it. Your base styles stay small, your overrides disappear, and your layout degrades gracefully. Let the content choose your breakpoints, express them in rem, and lean on intrinsic grids, fluid values, and container queries so you need fewer breakpoints overall.
Start your next component at 360px wide and let it grow. You'll be surprised how much CSS you no longer need to write.


