惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Martin Fowler
Martin Fowler
Last Week in AI
Last Week in AI
罗磊的独立博客
阮一峰的网络日志
阮一峰的网络日志
博客园 - 【当耐特】
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
雷峰网
雷峰网
博客园_首页
人人都是产品经理
人人都是产品经理
量子位
美团技术团队
The Cloudflare Blog
小众软件
小众软件
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
M
MIT News - Artificial intelligence
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
博客园 - Franky

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Mobile CSS consistency: all best practices in 2026
Odilon HUGON · 2026-05-15 · via DEV Community

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">

Enter fullscreen mode Exit fullscreen mode

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%;
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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 */
}

Enter fullscreen mode Exit fullscreen mode

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); }

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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%;
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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 */
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
    /* ... */
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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%;
}

Enter fullscreen mode Exit fullscreen mode

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.