# CLAUDE.md — Redesign Mobile & CSS Design
> Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour guider le travail sur le responsive mobile et le design CSS.
---
## Section 1 : Workflow de redesign mobile
### Audit initial — par où commencer
Avant de toucher une seule ligne de CSS, faire un passage rapide dans cet ordre :
1. **Balise viewport** — présente ? `width=device-width, initial-scale=1` ? Pas de `user-scalable=no` ?
2. **font-size sur les inputs** — inspecter les champs `<input>`, `<textarea>`, `<select>`. Si < 16px : zoom iOS garanti.
3. **Overflow horizontal** — ouvrir Chrome DevTools en mode mobile, regarder si une scrollbar apparaît en bas. Traquer avec `* { outline: 1px solid red; }`.
4. **Touch targets** — tapper sur les boutons et liens de nav en mode émulation. Tout ce qui fait < 44px de hauteur est à corriger.
5. **Textes justifiés** — rechercher `text-align: justify` dans le CSS. Supprimer ou limiter au desktop uniquement.
### Lighthouse mobile — cibles
Lancer Lighthouse en mode "Mobile" (pas Desktop) dans DevTools :
| Métrique | Cible |
|----------|-------|
| Performance | ≥ 80 |
| Accessibility | ≥ 90 |
| Best Practices | ≥ 90 |
| SEO | ≥ 90 |
Les alertes à corriger en priorité : tap targets trop petits, contraste insuffisant, viewport manquant, inputs sans label.
### DevTools device emulation
- Tester avec les profils : **iPhone SE (375px)**, **iPhone 14 (390px)**, **Pixel 7 (412px)**, **iPad (768px)**
- Activer "Show media queries" pour voir les breakpoints actifs
- Tester en mode paysage (landscape)
- Simuler réseau "Slow 4G" pour détecter les problèmes de performance
### Test sur vrai device
L'émulation ne remplace pas le test réel. Points spécifiques à vérifier sur un vrai téléphone :
- Zoom automatique iOS sur les inputs (ne se voit qu'en vrai)
- Scroll momentum et zones de rebond
- Safe areas (encoche, barre de navigation)
- Clavier virtuel qui pousse le layout
### Checklist "mobile done"
- [ ] Aucun scroll horizontal non intentionnel
- [ ] Tous les éléments interactifs ≥ 44×44px
- [ ] Aucun input avec font-size < 16px
- [ ] Viewport meta tag correct
- [ ] Pas de `text-align: justify` sur mobile
- [ ] Corps de texte ≥ 16px, line-height ≥ 1.5
- [ ] Images avec `max-width: 100%`
- [ ] Dark mode fonctionne (si implémenté)
- [ ] Animations respectent `prefers-reduced-motion`
- [ ] Lighthouse Accessibility ≥ 90
---
## Section 2 : Outils de test mobile — screenshots, émulation, MCP
### Installation des dépendances de test
```bash
# Playwright (recommandé — multi-navigateur)
npm init -y
npm install playwright
npx playwright install chromium
# OU Puppeteer (Chromium uniquement)
npm install puppeteer
```
### Screenshots mobile automatisés
```javascript
// screenshot-mobile.mjs
import { chromium } from 'playwright';
const devices = [
{ name: 'iPhone SE', width: 375, height: 667, scale: 2 },
{ name: 'iPhone 14', width: 390, height: 844, scale: 3 },
{ name: 'Pixel 7', width: 412, height: 915, scale: 2.625 },
{ name: 'iPad', width: 768, height: 1024, scale: 2 },
];
const url = process.argv[2] || 'http://localhost:8000';
const browser = await chromium.launch();
for (const device of devices) {
const context = await browser.newContext({
viewport: { width: device.width, height: device.height },
deviceScaleFactor: device.scale,
isMobile: device.width < 768,
hasTouch: device.width < 768,
});
const page = await context.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
await page.screenshot({
path: `screenshot-${device.name.toLowerCase().replace(/\s/g, '-')}.png`,
fullPage: true,
});
console.log(`✓ ${device.name} (${device.width}px)`);
await context.close();
}
await browser.close();
```
Usage :
```bash
node screenshot-mobile.mjs http://localhost:8000
# Génère : screenshot-iphone-se.png, screenshot-iphone-14.png, etc.
```
Claude Code peut lire ces screenshots pour vérifier visuellement le rendu.
### Dark mode et reduced motion screenshots
```javascript
// screenshot-modes.mjs — variantes dark mode et reduced motion
import { chromium } from 'playwright';
const url = process.argv[2] || 'http://localhost:8000';
const browser = await chromium.launch();
// Dark mode - iPhone 14
const darkCtx = await browser.newContext({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
colorScheme: 'dark',
});
const darkPage = await darkCtx.newPage();
await darkPage.goto(url, { waitUntil: 'networkidle' });
await darkPage.screenshot({ path: 'screenshot-dark-mode.png', fullPage: true });
console.log('✓ Dark mode');
await darkCtx.close();
// Reduced motion - iPhone 14
const reducedCtx = await browser.newContext({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
reducedMotion: 'reduce',
});
const reducedPage = await reducedCtx.newPage();
await reducedPage.goto(url, { waitUntil: 'networkidle' });
await reducedPage.screenshot({ path: 'screenshot-reduced-motion.png', fullPage: true });
console.log('✓ Reduced motion');
await reducedCtx.close();
await browser.close();
```
### MCP Servers utiles
**Puppeteer MCP** (`@modelcontextprotocol/server-puppeteer`) :
- Permet à Claude Code de naviguer sur un site, prendre des screenshots, inspecter le DOM
- Configuration dans `.claude/settings.json` :
```json
{
"mcpServers": {
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}
}
```
**Playwright MCP** (community `playwright-mcp`) :
- Même principe, multi-navigateur
- Utile pour tester le rendu sur différents moteurs (Chromium, Firefox, WebKit/Safari)
### Lighthouse en CLI
```bash
# Installation
npm install -g lighthouse
# Audit mobile
lighthouse http://localhost:8000 --preset=perf --form-factor=mobile --output=html --output-path=./lighthouse-mobile.html
# Audit mobile en JSON (parsable)
lighthouse http://localhost:8000 --form-factor=mobile --output=json --output-path=./lighthouse-mobile.json
```
Claude Code peut lire le rapport HTML pour identifier les problèmes.
### Debug avec Chrome DevTools Protocol
Playwright et Puppeteer exposent le CDP (Chrome DevTools Protocol), ce qui permet un accès programmatique à :
- **Network throttling** — simuler une connexion Slow 3G/4G pour détecter les goulots de chargement
- **CSS coverage** — identifier les règles CSS jamais appliquées (dead code)
- **Performance traces** — capturer une trace complète pour analyser le rendu frame par frame
---
## Section 3 : Règles CSS Mobile — référence complète
### Viewport meta tag
```html
<!-- ✅ Correct -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Si on utilise safe-area-inset -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<!-- ❌ Ne jamais faire : empêche le zoom pour les malvoyants -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1">
```
### Reset de base
```css
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
html, body {
overflow-x: hidden;
max-width: 100%;
}
img, video, svg {
max-width: 100%;
height: auto;
display: block;
}
```
### Alignement des textes
Règle simple : **gauche par défaut, centré uniquement pour les CTA, jamais justify sur mobile**.
```css
/* Base mobile */
h1, h2, h3, h4 {
text-align: left;
}
p, li {
text-align: left;
}
/* CTA : centré */
.cta-wrapper {
text-align: center;
}
/* Justify uniquement sur desktop avec césure */
@media (min-width: 768px) {
.prose p {
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
}
```
### Touch targets — 44×44px minimum
```css
/* Boutons */
.btn {
min-height: 44px;
min-width: 44px;
padding: 12px 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Liens de navigation */
nav a {
display: block;
min-height: 44px;
padding: 12px 16px;
line-height: 20px;
}
/* Icônes cliquables */
.icon-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
/* Zone de tap élargie sans changer l'apparence */
.small-icon {
position: relative;
width: 20px;
height: 20px;
}
.small-icon::after {
content: '';
position: absolute;
inset: -12px;
}
```
### Font-size — prévention zoom iOS et lisibilité
```css
/* ❌ Moins de 16px sur les inputs = zoom iOS automatique */
/* ✅ Minimum absolu sur les inputs */
input,
textarea,
select {
font-size: 16px;
}
/* Si taille différente souhaitée sur desktop */
@media (min-width: 768px) {
input,
textarea,
select {
font-size: 14px;
}
}
/* Corps de texte */
body {
font-size: 16px;
line-height: 1.6;
}
/* Titres fluides avec clamp() */
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); }
```
### Media queries — mobile-first uniquement
```css
/* ✅ Mobile-first : base = mobile, on ajoute pour les grands écrans */
.card {
display: block;
padding: 16px;
}
@media (min-width: 768px) {
.card {
display: flex;
padding: 24px;
}
}
@media (min-width: 1024px) {
.card {
padding: 32px;
}
}
/* ❌ Desktop-first : à éviter */
.card {
display: flex;
padding: 32px;
}
@media (max-width: 767px) {
.card {
display: block;
padding: 16px;
}
}
```
Breakpoints standards à utiliser : `480px`, `768px`, `1024px`, `1280px`.
### Prévention overflow-x
```css
html, body {
overflow-x: hidden;
max-width: 100%;
}
/* Tableaux : scroll plutôt que casser le layout */
.table-wrapper {
overflow-x: auto;
}
table {
min-width: 600px;
width: 100%;
}
/* Debug : identifier l'élément qui déborde */
* { outline: 1px solid red; }
```
Attention : `overflow-x: hidden` sur `body` peut casser `position: sticky`. Si un sticky ne fonctionne plus, isoler `overflow-x: hidden` sur un wrapper dédié.
### Images et aspect-ratio
```css
img {
max-width: 100%;
height: auto;
display: block;
}
/* Ratio fixe pour éviter le CLS (Cumulative Layout Shift) */
.card-image {
aspect-ratio: 4 / 3;
width: 100%;
object-fit: cover;
}
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}
/* Ratio différent sur mobile */
@media (max-width: 767px) {
.hero-image {
aspect-ratio: 4 / 3;
}
}
```
### Flex et Grid — pièges courants
```css
/* Flex : min-width: 0 obligatoire pour éviter les débordements de texte */
.flex-item {
min-width: 0;
}
/* Flex wrap automatique */
.card-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.card-grid .card {
flex: 1 1 280px;
min-width: 0;
}
/* Grid responsive sans media query */
.auto-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
/* Grid : colonne unique sur mobile, deux colonnes sur desktop */
.two-col {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
@media (min-width: 768px) {
.two-col {
grid-template-columns: 1fr 1fr;
}
}
```
### Safe area insets (encoche, barre de navigation)
```css
/* Nécessite viewport-fit=cover dans la balise meta */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 56px;
padding-bottom: env(safe-area-inset-bottom);
background: var(--color-bg);
}
.top-header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding-top: env(safe-area-inset-top);
}
.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);
}
```
Si `viewport-fit=cover` n'est pas défini, les valeurs valent 0 — ces règles n'ont aucun effet négatif.
### prefers-reduced-motion
```css
/* Animations définies normalement */
.fade-in {
animation: fadeIn 0.4s ease;
}
/* Suppression globale si l'utilisateur le demande */
@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;
}
}
/* Approche plus granulaire */
@media (prefers-reduced-motion: reduce) {
.fade-in {
opacity: 1;
animation: none;
}
}
```
### prefers-color-scheme — dark mode
```css
: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);
}
img:not([src*=".svg"]) {
filter: brightness(0.9);
}
}
/* Toggle manuel qui coexiste avec la préférence système */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg: #0f172a;
/* ... */
}
}
[data-theme="dark"] {
--color-bg: #0f172a;
/* ... */
}
[data-theme="light"] {
--color-bg: #ffffff;
/* ... */
}
```
### Formulaires
```css
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="number"],
textarea,
select {
width: 100%;
font-size: 16px; /* obligatoire — empêche zoom iOS */
padding: 12px 16px;
min-height: 44px;
border-radius: 8px;
border: 1px solid var(--color-border);
-webkit-appearance: none;
appearance: none;
background: var(--color-bg);
color: var(--color-text);
}
label {
display: block;
font-size: 16px;
margin-bottom: 6px;
font-weight: 500;
}
.form-group {
margin-bottom: 20px;
}
```
Attributs HTML à ne pas oublier (CSS ne peut pas les remplacer) :
```html
<!-- Clavier numérique -->
<input type="text" inputmode="numeric">
<!-- Clavier décimal -->
<input type="text" inputmode="decimal">
<!-- Clavier email avec @ direct -->
<input type="email" inputmode="email">
<!-- Clavier URL -->
<input type="url" inputmode="url">
```
### Espacement latéral minimum
```css
.container {
padding-left: 16px; /* minimum absolu sur mobile */
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;
}
}
```
---
## Section 4 : Conventions CSS — règles génériques
### CSS custom properties pour le theming
Toujours définir les valeurs répétées en custom properties. Jamais de couleurs ou d'espacements en dur dispersés dans le CSS.
```css
:root {
/* Couleurs */
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
--color-surface: #f9fafb;
--color-danger: #dc2626;
--color-success: #16a34a;
/* Espacements */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-2xl: 48px;
/* Typographie */
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--line-height-base: 1.6;
--line-height-tight: 1.3;
/* Rayons */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
/* Ombres */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-fast: 0.15s ease;
--transition-base: 0.25s ease;
}
```
### Conventions de nommage
Être cohérent. BEM ou utility-first, pas les deux mélangés dans le même projet.
**BEM** (conseillé pour les composants isolés) :
```css
/* Block */
.card {}
/* Element */
.card__title {}
.card__body {}
.card__footer {}
/* Modifier */
.card--featured {}
.card--compact {}
.card__title--large {}
```
**Ne pas faire** :
```css
/* Mélange de conventions — illisible */
.card {}
.cardTitle {}
.card-body {}
.card_footer--active {}
```
### Accessibilité
**focus-visible** — ne jamais supprimer l'outline sans le remplacer :
```css
/* ❌ À ne jamais faire */
* { outline: none; }
:focus { outline: none; }
/* ✅ Remplacer l'outline natif par un style custom */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 2px;
}
/* Supprimer l'outline seulement pour les clics souris (pas clavier) */
:focus:not(:focus-visible) {
outline: none;
}
```
**Contrastes WCAG AA** :
| Contexte | Ratio minimum |
|----------|--------------|
| Texte normal (< 18px) | 4.5:1 |
| Texte large (≥ 18px ou ≥ 14px bold) | 3:1 |
| Composants UI, icônes | 3:1 |
Outils : [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/), Chrome DevTools accessibilité.
### Performance CSS
Propriétés coûteuses à éviter sur les éléments animés ou nombreux :
```css
/* ❌ Déclenche du repaint sur chaque frame d'animation */
.card:hover {
box-shadow: 0 8px 30px rgba(0,0,0,0.2);
}
/* ✅ Mieux : préparer l'ombre au chargement, changer l'opacité */
.card {
box-shadow: 0 8px 30px rgba(0,0,0,0);
transition: box-shadow var(--transition-base);
}
.card:hover {
box-shadow: 0 8px 30px rgba(0,0,0,0.2);
}
/* ❌ filter sur beaucoup d'éléments = coûteux */
.grid-item {
filter: grayscale(100%);
}
/* ✅ Utiliser opacity ou transform — composités sur le GPU */
.element {
transform: translateY(0);
opacity: 1;
transition: transform var(--transition-base), opacity var(--transition-base);
}
.element.hidden {
transform: translateY(8px);
opacity: 0;
}
```
Propriétés "composites" (GPU, performantes pour les animations) : `transform`, `opacity`.
Propriétés qui déclenchent reflow (coûteuses) : `width`, `height`, `margin`, `padding`, `top`, `left`.
### CSS moderne à privilégier
```css
/* clamp() — tailles fluides sans media queries */
h1 { font-size: clamp(1.5rem, 5vw, 2.5rem); }
.container { padding: clamp(16px, 4vw, 48px); }
/* gap — plus lisible que margin sur les enfants */
.grid {
display: flex;
gap: 24px; /* ✅ */
}
/* éviter : .grid > * + * { margin-left: 24px; } */
/* aspect-ratio — plus fiable que le padding-top hack */
.video-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
}
/* Propriétés logiques — pour le multilinguisme (RTL/LTR) */
.card {
margin-inline: auto; /* = margin-left + margin-right */
padding-block: 24px; /* = padding-top + padding-bottom */
border-inline-start: 3px solid var(--color-primary); /* = border-left en LTR */
}
/* inset — remplace top/right/bottom/left */
.overlay {
position: fixed;
inset: 0; /* = top: 0; right: 0; bottom: 0; left: 0; */
}
```
### Règle sur `!important`
`!important` est accepté uniquement dans deux cas :
1. Classes utilitaires (`.sr-only`, `.hidden`, `.visually-hidden`) — par convention
2. Override de styles tiers qu'on ne contrôle pas (plugins, librairies)
En dehors de ces cas, un `!important` signale un problème de spécificité à corriger à la source.
### HTML sémantique en premier
Styliser des éléments sémantiques corrects est plus simple et plus accessible que styler des `<div>` partout.
```html
<!-- ❌ -->
<div class="nav">
<div class="nav-item" onclick="navigate()">Accueil</div>
</div>
<!-- ✅ -->
<nav>
<a href="/">Accueil</a>
</nav>
```
```html
<!-- ❌ -->
<div class="button" onclick="submit()">Envoyer</div>
<!-- ✅ -->
<button type="submit">Envoyer</button>
```
Les éléments sémantiques apportent gratuitement : accessibilité clavier, rôles ARIA implicites, compatibilité lecteur d'écran.
---
## Section 5 : Checklist de test
### Devices à tester
| Device | Largeur | Priorité |
|--------|---------|----------|
| iPhone SE (2020/2022) | 375px | Haute — écran le plus étroit mainstream |
| iPhone 14 / 15 | 390px | Haute — référence iOS courante |
| Android mid-range (Pixel 6a, Samsung A54) | 360–412px | Haute |
| iPad / iPad Mini | 768px | Moyenne |
| Desktop laptop | 1280–1440px | Baseline |
### Modes à tester
- [ ] **Portrait** (défaut)
- [ ] **Paysage (landscape)** — vérifier que rien ne déborde, les éléments fixes ne prennent pas trop de hauteur
- [ ] **Dark mode** — activer dans les réglages du device ou de l'OS
- [ ] **Reduced motion** — activer "Réduire les animations" dans l'accessibilité OS
- [ ] **Zoom texte à 150%** — dans les réglages d'accessibilité du navigateur
### Lighthouse — cibles par métrique
Lancer en mode "Mobile" :
| Score | Cible |
|-------|-------|
| Performance | ≥ 80 |
| Accessibility | ≥ 90 |
| Best Practices | ≥ 90 |
| SEO | ≥ 90 |
Points d'accessibilité à corriger en priorité :
- Contraste insuffisant (auto-détecté)
- Images sans `alt`
- Éléments interactifs sans label
- Liens avec un texte générique ("cliquez ici", "en savoir plus")
- Tap targets trop petits
### Vérification lecteur d'écran (bases)
Sur iOS : activer **VoiceOver** (triple-clic bouton latéral).
Sur Android : activer **TalkBack**.
Parcourir la page avec les gestes de base :
- Le titre principal est-il annoncé correctement ?
- Les boutons ont-ils un libellé clair ?
- Les images décoratives ont-elles `alt=""`?
- Les champs de formulaire ont-ils des labels associés ?
### Checklist finale avant livraison
- [ ] Aucun scroll horizontal sur aucun device testé
- [ ] Tous les éléments interactifs accessibles au clavier (Tab + Enter/Space)
- [ ] focus-visible visible et distinctif
- [ ] Tous les inputs avec font-size ≥ 16px
- [ ] Textes avec contraste ≥ 4.5:1 (normal) ou 3:1 (grand)
- [ ] Images avec `alt` descriptif ou `alt=""` si décoratif
- [ ] Formulaires avec `<label>` associé à chaque input
- [ ] Dark mode : texte lisible, pas de blanc éblouissant
- [ ] Reduced motion : aucune animation qui se joue quand même
- [ ] Lighthouse Accessibility ≥ 90
- [ ] Pas de zoom automatique iOS sur les inputs
- [ ] Touch targets ≥ 44×44px
---
## Section 6 : Pièges iOS et Android
### 100vh sur Safari — le piège classique
```css
/* ❌ 100vh inclut la barre d'adresse Safari → contenu coupé */
.hero { height: 100vh; }
/* ✅ Utiliser svh/dvh (Baseline 2023) */
.hero {
height: 100vh; /* fallback navigateurs anciens */
height: 100svh; /* small viewport height — le plus sûr */
}
/* dvh = dynamique, s'adapte quand les barres apparaissent/disparaissent */
.fullscreen-overlay {
height: 100dvh;
}
```
### -webkit-tap-highlight-color — le flash bleu au tap
```css
/* Supprimer le highlight par défaut sur mobile WebKit */
* {
-webkit-tap-highlight-color: transparent;
}
/* Restaurer un feedback visible pour l'accessibilité */
button:active, a:active {
opacity: 0.7;
}
```
### touch-action — contrôle des gestes
```css
/* Empêcher le double-tap zoom sur les boutons (garde le pinch-zoom) */
.btn, a {
touch-action: manipulation;
}
/* Zone de carte/canvas : gérer les gestes manuellement */
.map-container {
touch-action: none;
}
/* Scroll vertical uniquement (ex: carousel horizontal géré en JS) */
.vertical-only {
touch-action: pan-y;
}
```
### Rubber-banding iOS et scroll des modals
```css
/* Empêcher le scroll de traverser un modal */
.modal-open body {
overflow: hidden;
position: fixed; /* corrige le bleed-through iOS */
width: 100%;
}
/* Contenir le scroll dans un élément sans propager au parent */
.modal-body,
.dropdown-menu,
.sidebar-scroll {
overscroll-behavior: contain;
}
/* Désactiver le pull-to-refresh (app-like) */
html {
overscroll-behavior-y: none;
}
```
### iOS input appearance — reset complet
```css
/* Empêcher le style natif iOS de surcharger le vôtre */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="number"],
input[type="search"],
input[type="date"],
input[type="time"],
textarea,
select {
-webkit-appearance: none;
appearance: none;
}
```
### Android text-size-adjust
```css
/* Chrome Android gonfle le texte dans les conteneurs larges (> 217 chars) */
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
```
---
## Section 7 : Font loading et images — performance CLS/LCP
### Stratégie font-display
```css
/* ❌ Par défaut (auto) : texte invisible jusqu'à 3s (FOIT) */
/* ✅ swap : affiche le fallback immédiatement, swap quand chargé */
@font-face {
font-family: "MyFont";
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap;
font-weight: 400;
font-style: normal;
}
/* Pour les polices décoratives non-critiques */
@font-face {
font-family: "DisplayFont";
src: url('/fonts/display.woff2') format('woff2');
font-display: optional; /* navigateur peut ignorer sur connexion lente */
}
```
### Réduire le CLS avec size-adjust sur le fallback
```css
/* Aligner les métriques du fallback sur la police web pour éviter le reflow */
@font-face {
font-family: "MyFont-fallback";
src: local("Arial");
size-adjust: 105%;
ascent-override: 90%;
descent-override: 25%;
line-gap-override: 0%;
}
body {
font-family: "MyFont", "MyFont-fallback", sans-serif;
}
```
### WOFF2 uniquement
```css
/* En 2025 : seul WOFF2 nécessaire — support > 95%, 30% mieux compressé */
@font-face {
src: url('/fonts/myfont.woff2') format('woff2');
/* Plus besoin de woff, ttf, ou eot */
}
```
### Preload et preconnect
```html
<!-- Preload des polices above-the-fold — critique pour le LCP -->
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
<!-- Preconnect pour Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
```
### Images — width/height obligatoires
```html
<!-- ✅ TOUJOURS déclarer width/height — source #1 de CLS -->
<img src="photo.jpg" width="800" height="600" alt="...">
<!-- Le CSS peut overrider la taille affichée — le navigateur utilise le ratio -->
```
### fetchpriority — indiquer l'image LCP
```html
<!-- Image hero/LCP : chargement eager + haute priorité réseau -->
<img src="hero.jpg" fetchpriority="high" loading="eager" alt="...">
<!-- Images below-the-fold : lazy + basse priorité -->
<img src="gallery.jpg" loading="lazy" fetchpriority="low" alt="...">
```
### srcset et picture — servir la bonne résolution
```html
<!-- srcset : le navigateur choisit la meilleure source -->
<img
src="image-800.jpg"
srcset="image-400.jpg 400w, image-800.jpg 800w, image-1600.jpg 1600w"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 800px"
alt="..."
width="800" height="600"
loading="lazy"
>
<!-- picture : servir AVIF/WebP avec fallback -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="..." width="800" height="600" loading="lazy">
</picture>
```
---
## Section 8 : Scroll et navigation mobile
### scrollbar-gutter — éviter le CLS de la scrollbar
```css
/* Réserver l'espace de la scrollbar même quand elle n'est pas visible */
html {
scrollbar-gutter: stable;
}
```
### scroll-behavior — smooth scroll accessible
```css
/* Appliquer uniquement si l'utilisateur n'a pas demandé reduced motion */
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
```
### scroll-snap — carousels CSS natifs
```css
/* Carousel horizontal */
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding: 0 16px;
-webkit-overflow-scrolling: touch;
}
.carousel-item {
flex-shrink: 0;
scroll-snap-align: start;
width: 80%;
}
/* Sections plein écran — 'proximity' plutôt que 'mandatory' pour ne pas piéger */
.sections-wrapper {
height: 100svh;
overflow-y: auto;
scroll-snap-type: y proximity;
}
.section-fullpage {
height: 100svh;
scroll-snap-align: start;
}
```
---
## Section 9 : CSS moderne (Baseline 2024-2025)
### Container queries — media queries pour composants
```css
/* Le parent déclare qu'il est un container */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* Le composant se style selon la largeur de SON container */
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}
/* ⚠️ Un container ne peut PAS se styler lui-même */
```
### text-wrap: balance et pretty
```css
/* Équilibrer la longueur des lignes dans les titres */
h1, h2, h3 {
text-wrap: balance;
}
/* Éviter les mots orphelins dans le corps de texte */
p {
text-wrap: pretty;
}
```
### CSS nesting natif (Baseline 2024)
```css
/* Plus besoin de préprocesseur */
.card {
padding: 16px;
& .card__title {
font-size: 1.25rem;
}
&:hover {
box-shadow: var(--shadow-md);
}
@media (min-width: 768px) {
padding: 24px;
}
}
```
### light-dark() — dark mode simplifié
```css
:root {
color-scheme: light dark;
}
.card {
background: light-dark(#ffffff, #1e293b);
color: light-dark(#1a1a1a, #e2e8f0);
border-color: light-dark(#e5e7eb, #374151);
}
```
### color-mix() — dériver des variantes d'une couleur
```css
:root {
--color-primary: #2563eb;
--color-primary-light: color-mix(in srgb, var(--color-primary) 20%, white);
--color-primary-dark: color-mix(in srgb, var(--color-primary) 80%, black);
}
```
### @layer — contrôle de la cascade
```css
/* Déclarer l'ordre : les layers bas ont moins de priorité */
@layer reset, base, components, utilities;
@layer reset {
* { box-sizing: border-box; margin: 0; }
}
/* Utile pour isoler du CSS tiers dans un layer faible */
@layer vendor {
@import url("third-party.css");
}
/* Les styles hors-layer gagnent toujours sur les styles en layer */
```
### View transitions (Baseline 2025)
```css
/* Toujours conditionner à prefers-reduced-motion */
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
}
}
/* Nommer des éléments pour des transitions ciblées */
.hero-image {
view-transition-name: hero;
}
/* Cross-document (MPA) */
@view-transition {
navigation: auto;
}
```
---
## Section 10 : Performance CSS avancée
### content-visibility: auto — skip du rendu hors-écran
```css
/* Accélère le rendu initial des pages longues */
.section-below-fold {
content-visibility: auto;
contain-intrinsic-block-size: auto 500px; /* OBLIGATOIRE : réserver l'espace */
}
/* ⚠️ Safari : find-in-page ne trouve pas le contenu caché */
/* ⚠️ Ne pas utiliser content-visibility: hidden — retire de l'arbre d'accessibilité */
```
### CSS containment — isoler les sous-arbres coûteux
```css
/* contain: content = layout + paint + style */
.widget {
contain: content;
}
/* Utile pour les listes dynamiques */
.feed-item {
contain: layout paint;
}
```
### will-change — avec parcimonie
```css
/* ❌ JAMAIS global — explose la mémoire GPU sur mobile */
* { will-change: transform; }
/* ✅ Uniquement sur les éléments qui animent en boucle */
.spinner {
will-change: transform;
}
/* Idéalement : ajouter en JS avant l'animation, retirer après */
```
### Animer uniquement transform et opacity
```css
/* ❌ Déclenche un reflow à chaque frame */
.card:hover {
width: 110%;
margin-top: -5px;
}
/* ✅ transform et opacity sont composités GPU — jamais de reflow */
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: scale(1.02) translateY(-4px);
}
```
---
## Section 11 : Accessibilité avancée
### forced-colors (Windows High Contrast Mode)
```css
@media (forced-colors: active) {
/* Restaurer les bordures sur les éléments qui utilisent background-color */
.card {
border: 1px solid ButtonText;
}
/* Garder le contrôle sur les éléments custom */
.custom-checkbox {
forced-color-adjust: none;
}
}
```
### prefers-contrast
```css
@media (prefers-contrast: more) {
:root {
--color-border: #000000;
--color-text-muted: #444444;
}
.btn-ghost {
border-width: 2px;
}
}
```
### focus-visible — utiliser outline, pas box-shadow
```css
/* ❌ box-shadow invisible en Windows High Contrast */
:focus-visible {
box-shadow: 0 0 0 2px var(--color-primary);
}
/* ✅ outline survit au mode forced-colors */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Supprimer l'outline uniquement pour les clics souris */
:focus:not(:focus-visible) {
outline: none;
}
```
---
## Section 12 : Print styles
```css
@media print {
nav, .sidebar, .cookie-banner, .modal, footer, .btn, .social-links {
display: none !important;
}
a[href]::after {
content: " (" attr(href) ")";
font-size: 0.875em;
}
img, pre, blockquote, table {
break-inside: avoid;
}
h1, h2, h3 {
break-after: avoid;
}
body {
font-family: Georgia, serif;
font-size: 12pt;
color: #000;
background: #fff;
}
.container {
max-width: 100%;
padding: 0;
}
}
```
---
## Section 13 : Checklist complète mise à jour
### Devices à tester
| Device | Largeur | Priorité |
|--------|---------|----------|
| iPhone SE (2020/2022) | 375px | Haute — écran le plus étroit mainstream |
| iPhone 14 / 15 | 390px | Haute — référence iOS courante |
| Android mid-range (Pixel 6a, Samsung A54) | 360–412px | Haute |
| iPad / iPad Mini | 768px | Moyenne |
| Desktop laptop | 1280–1440px | Baseline |
### Modes à tester
- [ ] Portrait
- [ ] Paysage
- [ ] Dark mode
- [ ] Reduced motion
- [ ] Zoom texte à 150%
- [ ] Contrast élevé (forced-colors)
### Core Web Vitals — cibles
| Métrique | Cible | Impact |
|----------|-------|--------|
| LCP (Largest Contentful Paint) | < 2.5s | Image hero, fonts |
| CLS (Cumulative Layout Shift) | < 0.1 | width/height sur img, font size-adjust |
| INP (Interaction to Next Paint) | < 200ms | Animations GPU-only, pas de reflow |
### Lighthouse — cibles
| Score | Cible |
|-------|-------|
| Performance | ≥ 80 |
| Accessibility | ≥ 90 |
| Best Practices | ≥ 90 |
| SEO | ≥ 90 |
### Checklist finale
- [ ] Aucun scroll horizontal sur aucun device testé
- [ ] Tous les éléments interactifs accessibles au clavier (Tab + Enter/Space)
- [ ] focus-visible visible et utilise outline (pas box-shadow)
- [ ] Tous les inputs avec font-size ≥ 16px
- [ ] Textes avec contraste ≥ 4.5:1 (normal) ou 3:1 (grand)
- [ ] Images avec `alt` descriptif ou `alt=""` si décoratif
- [ ] Images avec `width` et `height` déclarés (CLS)
- [ ] Image LCP avec `fetchpriority="high"` et `loading="eager"`
- [ ] Formulaires avec `<label>` associé à chaque input
- [ ] Pas de `height: 100vh` sans fallback `100svh`
- [ ] `overscroll-behavior: contain` sur les overlays scrollables
- [ ] `-webkit-tap-highlight-color: transparent` appliqué
- [ ] `touch-action: manipulation` sur les boutons
- [ ] `font-display: swap` sur toutes les @font-face
- [ ] Polices web en WOFF2 uniquement
- [ ] Polices critiques preload dans le `<head>`
- [ ] Dark mode fonctionne (si implémenté)
- [ ] Animations respectent `prefers-reduced-motion`
- [ ] Print styles testés
- [ ] Lighthouse Accessibility ≥ 90
- [ ] LCP < 2.5s, CLS < 0.1, INP < 200ms
---
*Last updated: 2025-03 — Revoir si : nouvelles directives WCAG (3.0), changements des Core Web Vitals Google, ou nouveaux appareils avec safe areas non standards.*