Mobile CSS consistency: all best practices in 2026

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=1 without user-scalable=no
  • Box-sizing: border-box on 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: hidden on html/body, tables in a scrollable wrapper
  • Images: max-width: 100%, aspect-ratio to avoid CLS
  • Forms: width: 100% on inputs, appropriate inputmode in HTML
  • Flex items: min-width: 0 to avoid overflows
  • Safe areas: env(safe-area-inset-*) for fixed elements with viewport-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.

Comments (0)