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

推荐订阅源

WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
小众软件
小众软件
Recent Announcements
Recent Announcements
博客园 - 三生石上(FineUI控件)
Security Archives - TechRepublic
Security Archives - TechRepublic
W
WeLiveSecurity
Cloudbric
Cloudbric
博客园 - 司徒正美
美团技术团队
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
PCI Perspectives
PCI Perspectives
宝玉的分享
宝玉的分享
H
Help Net Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
Help Net Security
Help Net Security
Last Week in AI
Last Week in AI
S
Schneier on Security
N
News | PayPal Newsroom
B
Blog RSS Feed
L
LINUX DO - 最新话题
T
Troy Hunt's Blog
S
Secure Thoughts
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tenable Blog
S
Securelist
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
I
InfoQ
H
Heimdal Security Blog
Cisco Talos Blog
Cisco Talos Blog
F
Full Disclosure
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
K
Kaspersky official blog
T
Tailwind CSS Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs

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 Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
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.