Web App Manifest et installation PWA : A2HS sans App Store
Le Web App Manifest est un fichier JSON qui déclare votre application comme installable : icône sur l'écran d'accueil, mode plein écran sans barre de navigation, couleur de thème. C'est la différence entre un favori dans le navigateur et une vraie application qui s'ouvre comme une app native.
Nous expliquons ici comment configurer ce manifest avec vite-plugin-pwa, gérer le prompt d'installation sur Android et le cas particulier d'iOS Safari avec notre composable useInstallPrompt utilisé dans nos projets PWA.
CHAMPS DU MANIFEST
Ce que le manifest déclare au navigateur
Chaque champ a une incidence directe sur l'expérience d'installation et sur l'apparence de l'application une fois installée.
name / short_name
Le nom affiché sur l'écran d'accueil. name est le nom complet (dialogue d'installation), short_name est le nom court sous l'icône (limité à 12 caractères sur Android).
display: standalone
L'application s'ouvre sans barre d'adresse ni navigation du navigateur. L'utilisateur a l'impression d'une app native. fullscreen supprime aussi la barre de statut (heure, batterie), rarement souhaitable.
icons (192px et 512px)
Deux tailles minimum : 192x192 pour Android et 512x512 pour le splash screen. L'icône maskable permet à Android de découper l'icône dans n'importe quelle forme (rond, carré arrondi...) selon le launcher.
theme_color / background_color
theme_color colorise la barre d'état et le switcher d'apps sur Android. background_color est la couleur du splash screen pendant le chargement initial. Elle doit correspondre à la couleur de fond de l'app pour éviter le flash blanc.
start_url
L'URL ouverte quand l'utilisateur lance l'app depuis l'écran d'accueil. Mettre / dans la plupart des cas. Peut inclure un paramètre de tracking (/?source=pwa) pour distinguer les sessions PWA dans les analytics.
orientation
portrait pour les apps mobiles classiques, landscape pour les tablettes de présentation ou les jeux. La valeur any laisse l'appareil décider, rarement recommandé pour les interfaces métier qui n'ont pas été conçues pour les deux orientations.
COMPORTEMENT PAR PLATEFORME
Android et iOS : deux comportements très différents
La gestion du prompt d'installation est la principale différence entre les deux plateformes. Android est standardisé, iOS nécessite une instruction manuelle.
Android Chrome
Automatisable
Chrome déclenche l'événement beforeinstallprompt automatiquement
quand les critères PWA sont remplis. On peut intercepeter cet événement et déclencher
le prompt natif Chrome au moment souhaité via promptInstall().
Prompt natif : boîte de dialogue Chrome reconnue par les utilisateurs
Événement appinstalled pour confirmer l'installation
display-mode: standalone pour détecter l'état installé
iOS Safari
Instruction manuelle
iOS Safari ne supporte pas beforeinstallprompt. L'installation est
manuelle : l'utilisateur appuie sur le bouton Partager (icône rectangle avec flèche)
puis "Ajouter à l'écran d'accueil". L'app doit afficher cette instruction.
Pas de beforeinstallprompt sur iOS
showIosHint pour afficher le guide d'installation
Supporté sur iOS 11.3+ (disponible sur tous les iPhones récents)
Notre approche : le composable
useInstallPrompt expose trois valeurs : deferredPrompt
(non nul si Android et l'événement est disponible), showIosHint
(vrai si iOS et non installée), isInstalled (vrai si déjà en mode standalone).
Le composant parent adapte l'interface selon ces trois cas sans logique supplémentaire.
Documentation développeurs
Code source : manifest et installation de nos POC
La configuration du manifest ce fait dans vite.config.ts et l'invite d'installation via le composable useInstallPrompt. C'est la base que nous réutilisons sur chaque nouveau projet PWA.
Configuration du manifest : vite-plugin-pwa
Extrait de vite.config.ts
Tous les champs du manifest sont déclarés dans vite.config.ts. Le plugin vite-plugin-pwa génère le fichier manifest.webmanifest et injecte la balise <link rel="manifest"> dans le HTML au build.
VitePWA({ registerType: 'autoUpdate', // Fichiers copiés dans dist/ et précachés par Workbox (logo SVG non inclus par défaut) includeAssets: ['icons/smartbooster-logo.svg'], manifest: { name: 'Démo PWA Terrain SmartBooster', short_name: 'Démo PWA Terrain', // ≤ 12 caractères recommandé description: 'Application terrain pour techniciens, démo offline', theme_color: '#2563eb', // barre de statut Android + switcher d'apps background_color: '#f8fafc', // couleur du splash screen au chargement display: 'standalone', // pas de barre d'adresse, look natif orientation: 'portrait', start_url: '/', icons: [ { src: '/icons/pwa-192x192.png', sizes: '192x192', type: 'image/png', }, { src: '/icons/pwa-512x512.png', sizes: '512x512', type: 'image/png', }, { src: '/icons/pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable', // permet à Android de découper l'icône }, ], },})
Icône maskable : Android adapte la forme de l'icône selon le launcher (rond, carré arrondi, goutte...). L'icône maskable doit avoir une zone de sécurité de 10% sur chaque bord pour éviter que le contenu soit découpé. L'outil maskable.app permet de tester le rendu avant déploiement.
useInstallPrompt : gestion du prompt Android et de l'hint iOS
composable partagé
Le composable intercepte l'événement beforeinstallprompt au niveau module (avant tout montage de composant) pour ne pas le rater s'il se déclenche tôt au chargement de la page. Il expose aussi showIosHint pour afficher une instruction manuelle sur iOS où cet événement n'existe pas.
import { isIOS, isAndroid } from '@/utils/deviceDetect.js'// Singleton module-level : les listeners sont enregistrés dès le premier import,// avant le montage du premier composant. Si beforeinstallprompt se déclenche// pendant le chargement (fréquent), on ne le rate pas.const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null)// Détection via display-mode : si l'app est déjà installée, ne pas afficher// la bannière d'installation.const isInstalled = ref(window.matchMedia('(display-mode: standalone)').matches)const isMobile = isIOS || isAndroid// Android Chrome : l'événement peut se déclencher avant le montage des composantswindow.addEventListener('beforeinstallprompt', (e) => { e.preventDefault() // empêche le mini infobar automatique de Chrome if (isMobile) deferredPrompt.value = e as BeforeInstallPromptEvent})// Mise à jour de isInstalled quand l'utilisateur accepte l'installationwindow.addEventListener('appinstalled', () => { deferredPrompt.value = null isInstalled.value = true})// iOS : pas de beforeinstallprompt. On affiche un hint textuel// ("Appuyez sur Partager → Ajouter à l'écran d'accueil") si l'app n'est pas installée.const showIosHint = ref(isMobile && isIOS && !isInstalled.value)export function useInstallPrompt() { async function promptInstall(): Promise<void> { if (!deferredPrompt.value) return await deferredPrompt.value.prompt() const { outcome } = await deferredPrompt.value.userChoice // outcome : 'accepted' | 'dismissed' deferredPrompt.value = null } return { deferredPrompt, isInstalled, showIosHint, promptInstall }}
Pourquoi au niveau module :beforeinstallprompt peut se déclencher très tôt, avant que le composant qui gère la bannière ne soit monté. Si le listener est dans onMounted, l'événement est perdu et la bannière ne s'affiche jamais, même si l'app est installable.
FAQ
Les réponses à vos questions
Et si vous ne trouvez pas ce que vous cherchez, nous serons ravis de vous répondre en direct lors d'un rendez-vous entre humains !
Non. Sur Android, Chrome affiche un prompt natif 'Ajouter à l'écran d'accueil' quand les critères PWA sont remplis (HTTPS, Service Worker actif, manifest valide). Sur iOS Safari, l'utilisateur appuie sur le bouton Partager puis 'Ajouter à l'écran d'accueil'. Pas de Store, pas de validation tierce. L'installation prend 5 secondes et l'app apparaît sur l'écran d'accueil comme n'importe quelle application native.
Chrome déclenche l'événement beforeinstallprompt automatiquement quand les critères PWA sont satisfaits (HTTPS, Service Worker enregistré, manifest avec name, short_name, start_url et icône 192px). Notre composable useInstallPrompt intercepte cet événement et affiche une bannière personnalisée au moment choisi. On peut aussi appeler promptInstall() à n'importe quel moment pour déclencher le prompt natif de Chrome.
Via window.matchMedia('(display-mode: standalone)').matches. Si la PWA est lancée depuis l'écran d'accueil (mode standalone), cette valeur est true. On peut aussi écouter l'événement appinstalled qui se déclenche juste après que l'utilisateur a accepté l'installation. Notre composable useInstallPrompt combine les deux pour masquer automatiquement la bannière d'installation quand l'app est déjà installée.
En théorie oui, en pratique Android peut modifier la forme de l'icône selon le launcher (rond, carré, goutte...). L'attribut purpose: 'maskable' sur l'icône 512px permet à Android de découper l'icône dans la forme souhaitée. Sans icône maskable, Android ajoute parfois un fond blanc autour de l'icône qui rend le résultat peu soigné. iOS respecte l'icône telle quelle avec des coins arrondis automatiques.
Sur Android, oui facilement via le mécanisme TWA (Trusted Web Activity) et l'outil Bubblewrap de Google. La PWA est encapsulée dans une app Android distribuable sur le Play Store sans modifier le code. Sur iOS, des outils tiers comme PWABuilder génèrent un wrapper Swift, mais l'approbation App Store reste incertaine et certaines PWA sont refusées. Pour les applications métier internes, l'installation directe depuis Safari est la solution la plus simple et elle fonctionne parfaitement.
Oui, avec display: standalone dans le manifest. L'app s'ouvre en plein écran sans barre d'adresse ni navigation du navigateur. L'utilisateur ne peut pas voir l'URL et l'expérience ressemble à une application native. Sur iOS, la barre de statut (heure, batterie) reste visible. Sur Android, elle peut être colorisée avec theme_color pour correspondre à l'identité visuelle de l'application.
Votre projet
Vous voulez une application qui s'installe comme une app native ?
Une PWA installée sur l'écran d'accueil offre la même expérience qu'une app Store, sans les contraintes de validation et de distribution. Discutons de votre projet.