We've all done it: build the interface on a 1440px screen, drag the window to shrink it, decide it "looks fine", and ship to production. Two days later, a user reports that the submit button is too small to tap, that justified text looks like swiss cheese on their iPhone SE, and that the login form triggers automatic Safari zoom. All of it avoidable. In 2026, mobile accounts for over 60% of global web traffic. It's no longer an edge case — it's the primary case.
Here are the concrete CSS rules, organized by theme. No theory: code that works and explanations for why the alternative fails.
Viewport and document base
Before any CSS, the meta viewport tag in the <head>.
Without it, mobile browsers simulate a desktop screen and reduce zoom — all responsive
CSS becomes useless.
<meta name="viewport" content="width=device-width, initial-scale=1">
Do not add user-scalable=no or maximum-scale=1. This is
a disastrous accessibility practice: it prevents visually impaired users from zooming.
Apple actually removed the effect of user-scalable=no in iOS 10 for this
reason. Useless and harmful.
Then, the basic reset that avoids surprises:
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
/* Prevents automatic text enlargement on rotation */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
Text alignment: the simple rule
Justified text (text-align: justify) looks clean on desktop with a 700px
column. On mobile with a 320px column, the hyphenation algorithm creates irregular
inter-word spacing, sometimes grotesque. Some mobile browsers don't handle automatic
hyphenation (hyphens: auto) correctly. Result: holes in the text, degraded
readability.
The rule to follow:
- Headings h1–h4: left-aligned. Easier to scan, the eye always knows where to resume.
- Body text / paragraphs: left-aligned. Justified on mobile is a bad idea in almost all cases.
- CTA buttons: centered. Catches the eye and is easier to tap with the thumb.
- Lists: left-aligned. The bullet/number already does the visual work.
/* Base rule — applies from mobile up */
h1, h2, h3, h4 {
text-align: left;
}
p, li {
text-align: left;
/* No justify on mobile */
}
/* If you want justify on desktop only */
@media (min-width: 768px) {
.prose p {
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
}
/* CTA buttons: centered */
.cta-wrapper {
text-align: center;
}
.btn-cta {
display: inline-block;
}
Touch targets: 44×44px minimum
Apple recommends 44×44pt, Google Material Design recommends 48×48dp. The basic rule: every interactive element (button, nav link, checkbox, clickable icon) must have a tap zone of at least 44×44px. A 12px-tall link is unusable with a thumb without a magnifier.
/* Buttons: guaranteed minimum size */
.btn {
min-height: 44px;
min-width: 44px;
padding: 12px 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Navigation links */
nav a {
display: block;
min-height: 44px;
padding: 12px 16px;
line-height: 20px;
}
/* Inline links in text: enlarged tap zone without touching the layout */
.text-link {
padding: 4px 0;
/* The tap zone will be taller than the text */
}
/* Clickable icons (burger menu, close, etc.) */
.icon-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
If an element is visually small (20×20px icon) but needs to be tappable, you can
enlarge the click zone without changing the appearance using ::after:
.small-icon {
position: relative;
width: 20px;
height: 20px;
}
.small-icon::after {
content: '';
position: absolute;
inset: -12px; /* enlarges the tap zone by 12px in each direction */
}
Font sizes: the iOS zoom pitfall
Safari on iOS automatically zooms into a form field when the font size is below 16px. It's "helpful" but it breaks the entire page layout. The fix is simple: never go below 16px on inputs.
/* Prevents automatic iOS zoom on inputs */
input,
textarea,
select {
font-size: 16px; /* Absolute minimum on mobile */
}
/* If you want a different size on desktop */
@media (min-width: 768px) {
input,
textarea,
select {
font-size: 14px; /* OK on desktop */
}
}
/* Body text: 16px minimum on mobile */
body {
font-size: 16px;
line-height: 1.6;
}
/* Headings: adapt without going too small */
h1 { font-size: clamp(1.5rem, 5vw, 2.5rem); }
h2 { font-size: clamp(1.25rem, 4vw, 2rem); }
h3 { font-size: clamp(1.1rem, 3vw, 1.5rem); }
clamp() is well supported (Chrome 79+, Firefox 75+, Safari 13.1+) and
avoids writing media queries for every heading. The syntax:
clamp(min, preferred, max). The "preferred" value in vw
allows a smooth progression between breakpoints.
Media queries: mobile-first or desktop-first?
This debate has a clear answer. Mobile-first wins, for concrete reasons:
- Base CSS applies to mobile devices without overriding media queries
- You don't override mobile styles with desktop — you build on them
- Slightly better performance: mobile browsers parse fewer conditional rules
/* ✅ Mobile-first: start from mobile, add for larger screens */
.card {
display: block;
padding: 16px;
}
@media (min-width: 768px) {
.card {
display: flex;
padding: 24px;
}
}
@media (min-width: 1024px) {
.card {
padding: 32px;
}
}
/* ❌ Desktop-first: start from desktop, override for mobile */
.card {
display: flex;
padding: 32px;
}
@media (max-width: 1023px) {
.card {
padding: 24px;
}
}
@media (max-width: 767px) {
.card {
display: block;
padding: 16px;
}
}
In practice, both approaches often coexist in a legacy codebase. What matters: be consistent within a given component, don't mix both approaches in the same rules.
Horizontal scroll: the overflow-x trap
Unintentional horizontal scroll on mobile is one of the most common and most painful
bugs to debug. Typical causes: an element with a fixed width larger than 100vw, a
forgotten white-space: nowrap, a translate going off-screen,
an image with no width constraint.
/* Global prevention */
html, body {
overflow-x: hidden;
max-width: 100%;
}
/* Responsive images — basic rule */
img, video, svg {
max-width: 100%;
height: auto;
}
/* Tables: allow horizontal scroll rather than breaking the layout */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
min-width: 600px; /* or whatever width is needed */
width: 100%;
}
Warning: overflow-x: hidden on body can interfere with
position: sticky elements. If a sticky stops working, it's usually
an ancestor with overflow that's blocking it. In that case, isolate
overflow-x: hidden on a specific wrapper rather than on body.
To debug a mysterious horizontal scroll:
/* Temporary debug: identify which element is overflowing */
* {
outline: 1px solid red;
}
Spacing and mobile-friendly padding
On mobile, space is precious but thumbs need room. The minimum lateral padding to avoid content flush against the edge: 16px. 20px is more comfortable.
.container {
padding-left: 16px;
padding-right: 16px;
max-width: 1200px;
margin: 0 auto;
}
@media (min-width: 768px) {
.container {
padding-left: 24px;
padding-right: 24px;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 32px;
padding-right: 32px;
}
}
/* Vertical spacing between sections */
.section {
padding-top: 40px;
padding-bottom: 40px;
}
@media (min-width: 768px) {
.section {
padding-top: 64px;
padding-bottom: 64px;
}
}
Mobile forms
Beyond the font-size: 16px already mentioned, forms on mobile have
other particularities.
/* Full-width inputs on mobile */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
textarea,
select {
width: 100%;
font-size: 16px; /* Prevents iOS zoom */
padding: 12px 16px;
min-height: 44px;
border-radius: 8px;
border: 1px solid #ccc;
/* Removes native iOS styling for more control */
-webkit-appearance: none;
appearance: none;
}
/* Numeric keyboard on mobile for appropriate fields */
/* (HTML, not CSS, but often forgotten) */
/* <input type="tel" inputmode="numeric"> */
/* <input type="text" inputmode="decimal"> */
/* Labels: readable size, sufficient spacing */
label {
display: block;
font-size: 16px;
margin-bottom: 6px;
font-weight: 500;
}
/* Spacing between fields */
.form-group {
margin-bottom: 20px;
}
The inputmode attribute (HTML, not CSS) is crucial for displaying the
right virtual keyboard: numeric for codes, decimal for
amounts, email for emails (shows the @ directly),
url for URLs. It costs nothing and significantly improves UX.
Flexbox and Grid: mobile pitfalls
Flexbox and Grid work well on mobile, but a few behaviors can surprise you.
/* Flex: avoid unintentional shrinking */
.flex-item {
flex-shrink: 0; /* If the element shouldn't shrink */
min-width: 0; /* Classic fix: flex items have min-width: auto by default,
which prevents text from wrapping */
}
/* Flex wrap to switch to column on mobile */
.card-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.card-grid .card {
flex: 1 1 280px; /* Grows, shrinks, base 280px → automatic wrap on mobile */
min-width: 0;
}
/* Responsive grid without media queries */
.auto-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
/* Grid: single column on mobile */
.two-col {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
@media (min-width: 768px) {
.two-col {
grid-template-columns: 1fr 1fr;
}
}
The min-width: 0 on flex items is a fix to know by heart. Without it,
a long word or a URL inside a flex item can overflow its container even with
overflow: hidden or word-break: break-all.
Safe area insets for notched phones
Since the iPhone X and its Android competitors, phones have notches, dynamic islands,
and rounded navigation bars that can overlap content. The
env(safe-area-inset-*) values let you account for them.
/* Extended viewport to use the full surface */
/* In the meta viewport: viewport-fit=cover is required */
/* <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> */
/* Fixed bottom navigation (common pattern on mobile) */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 56px;
padding-bottom: env(safe-area-inset-bottom);
background: #fff;
border-top: 1px solid #e0e0e0;
}
/* Fixed top header */
.top-header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding-top: env(safe-area-inset-top);
}
/* Main content: margin to avoid overlap */
.main-content {
padding-bottom: calc(56px + env(safe-area-inset-bottom));
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
If the site doesn't use viewport-fit=cover, safe area insets equal 0
and these rules have no effect — no risk of breaking anything by adding them.
Mobile navigation: hamburger and patterns
No magic CSS here, but a few rules that avoid classic mistakes:
/* Hamburger menu: hidden on desktop, visible on mobile */
.nav-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
@media (min-width: 768px) {
.nav-toggle {
display: none;
}
}
/* Mobile nav: clean transition */
.nav-menu {
display: none;
flex-direction: column;
position: fixed;
inset: 0;
background: #fff;
z-index: 1000;
padding: 60px 24px 24px;
overflow-y: auto;
}
.nav-menu.is-open {
display: flex;
}
/* Or version with transition */
.nav-menu {
position: fixed;
inset: 0;
background: #fff;
z-index: 1000;
padding: 60px 24px 24px;
overflow-y: auto;
transform: translateX(-100%);
transition: transform 0.25s ease;
}
.nav-menu.is-open {
transform: translateX(0);
}
/* Menu links: large enough to tap */
.nav-menu a {
display: block;
padding: 16px 0;
font-size: 18px;
border-bottom: 1px solid #f0f0f0;
text-decoration: none;
color: inherit;
}
@media (min-width: 768px) {
.nav-menu {
position: static;
transform: none;
display: flex;
flex-direction: row;
padding: 0;
gap: 24px;
}
.nav-menu a {
padding: 8px 0;
font-size: 16px;
border-bottom: none;
}
}
Performance: prefers-reduced-motion
Some users have configured their system to reduce animations (epilepsy, vestibular
disorders, personal preference). The prefers-reduced-motion media query
lets you respect their choice.
/* Default animations */
.fade-in {
animation: fadeIn 0.4s ease;
}
.slide-up {
animation: slideUp 0.3s ease;
}
/* Remove animations if the user requests it */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* More granular approach: remove only decorative animations */
@media (prefers-reduced-motion: reduce) {
.decorative-animation {
animation: none;
}
.fade-in {
opacity: 1; /* Show directly without fade */
}
}
On mobile, this has an additional impact: animations drain battery. Respecting
prefers-reduced-motion also means being more resource-efficient.
Dark mode with prefers-color-scheme
Dark mode has been OS-managed since iOS 13 and Android 10. Users expect web applications to respect their system preference.
/* CSS variables: clean approach for dark mode */
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
--color-surface: #f9fafb;
--color-primary: #2563eb;
--color-primary-text: #ffffff;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f172a;
--color-text: #e2e8f0;
--color-text-muted: #94a3b8;
--color-border: #1e293b;
--color-surface: #1e293b;
--color-primary: #3b82f6;
--color-primary-text: #ffffff;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
}
/* Usage */
body {
background-color: var(--color-bg);
color: var(--color-text);
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
box-shadow: var(--shadow);
}
/* Images: slightly dim in dark mode to reduce glare */
@media (prefers-color-scheme: dark) {
img:not([src*=".svg"]) {
filter: brightness(0.9);
}
}
If the site also implements a manual toggle (light/dark button), you need to manage
coexistence with the system preference. The classic approach: add a
data-theme="dark" class on <html> when the user
toggles manually, and scope the variables accordingly.
/* System preference */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg: #0f172a;
/* ... */
}
}
/* Manual override: dark */
[data-theme="dark"] {
--color-bg: #0f172a;
/* ... */
}
/* Manual override: light (takes priority over system dark preference) */
[data-theme="light"] {
--color-bg: #ffffff;
/* ... */
}
Scroll behavior and -webkit-overflow-scrolling
iOS native scroll has a momentum (inertia) effect that is sometimes missing in custom
scrollable areas. Historically, -webkit-overflow-scrolling: touch was
added to enable it. It's now the default behavior since iOS 13. The property is
deprecated but harmless if it lingers in legacy code.
/* Smooth scroll for anchor links */
html {
scroll-behavior: smooth;
}
/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
/* Custom scrollable area with scroll snap */
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 16px;
padding: 16px;
/* Hide scrollbar on desktop, keep the scroll */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.carousel::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.carousel-item {
scroll-snap-align: start;
flex-shrink: 0;
width: 280px;
}
Images: max-width and aspect-ratio
/* Responsive base */
img {
max-width: 100%;
height: auto;
display: block;
}
/* Avoid layout shift (CLS) with aspect-ratio */
.product-image {
aspect-ratio: 4 / 3;
width: 100%;
object-fit: cover;
border-radius: 8px;
}
/* Full-width hero image */
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}
/* On mobile, squarer ratio */
@media (max-width: 767px) {
.hero-image {
aspect-ratio: 4 / 3;
}
}
/* Images with loading background */
.lazy-image {
background-color: #f0f0f0;
aspect-ratio: 16 / 9;
width: 100%;
}
aspect-ratio is well supported (Chrome 88+, Firefox 89+, Safari 15+)
and advantageously replaces the old padding-top hack technique for maintaining the
ratio of an image before it loads.
Summary: rules to apply by default
- Viewport:
width=device-width, initial-scale=1withoutuser-scalable=no - Box-sizing:
border-boxon everything - Text-size-adjust:
100%to prevent automatic enlargement - Alignment: headings and body left-aligned, CTA buttons centered, no justify on mobile
- Touch targets: 44×44px minimum for everything clickable
- Input font-size: 16px minimum, otherwise automatic iOS zoom
- Body text: 16px minimum, line-height 1.5–1.6
- Media queries: mobile-first (
min-width) - Overflow-x:
hiddenon html/body, tables in a scrollable wrapper - Images:
max-width: 100%,aspect-ratioto avoid CLS - Forms:
width: 100%on inputs, appropriateinputmodein HTML - Flex items:
min-width: 0to avoid overflows - Safe areas:
env(safe-area-inset-*)for fixed elements withviewport-fit=cover - prefers-reduced-motion: remove or attenuate animations
- prefers-color-scheme: CSS variables for dark mode
- Scroll snap: for native horizontal carousels
None of these rules are revolutionary. What is, is applying them systematically from the start of the project rather than fixing them one by one after user reports.