Building Native-Like Web Apps: Installation, Badges, and Sharing with PWA APIs
The line between web and native applications continues to blur. Users expect web apps to behave like native applicationsโinstallable from the home screen, capable of displaying notification badges, and able to share content through native sharing dialogs. Progressive Web Apps (PWAs) make this possible through modern web APIs.
Three features in particular transform how users perceive and interact with web applications: the ability to install apps directly from the browser, displaying notification badges to indicate unread content, and leveraging native sharing capabilities. Together, these features create experiences that rival native applications while maintaining the web’s universal accessibility.
In this guide, we’ll explore how to implement these three powerful PWA features, complete with practical code examples and best practices.
Prerequisites and Requirements
Before implementing PWA features, ensure your application meets these requirements:
- HTTPS: All PWA features require secure HTTPS connections (except localhost for development)
- Service Worker: Must be registered and functional
- Web App Manifest: Required for installation and metadata
- Valid icons: Properly sized and formatted icons for installation
Feature 1: App Installation (Add to Home Screen)
App installation is the gateway to native-like experiences. It allows users to install your web app directly from the browser, creating a home screen icon and launching the app in standalone mode.
Understanding the Installation Flow
The installation process involves several components:
- Web App Manifest: Describes your app (name, icons, colors, start URL)
- Service Worker: Enables offline functionality and background features
- beforeinstallprompt event: Triggered when the browser detects an installable app
- User interaction: User explicitly installs the app
Creating the Web App Manifest
The manifest is a JSON file that tells the browser how to display your app:
{
"name": "My Awesome App",
"short_name": "AwesomeApp",
"description": "A web app that does awesome things",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#2196F3",
"background_color": "#FFFFFF",
"icons": [
{
"src": "/images/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/images/screenshot-540x720.png",
"sizes": "540x720",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/images/screenshot-1280x720.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
],
"categories": ["productivity"],
"shortcuts": [
{
"name": "Create Note",
"short_name": "New Note",
"description": "Create a new note quickly",
"url": "/?action=new",
"icons": [
{
"src": "/images/new-note-icon.png",
"sizes": "192x192"
}
]
}
]
}
Key manifest properties:
- display:
standalonemakes the app feel native (no browser UI) - theme_color: Colors the browser UI to match your app
- icons: Multiple sizes for different contexts (home screen, splash screen, etc.)
- purpose:
maskableicons adapt to different device shapes - screenshots: Show app preview during installation
- shortcuts: Quick actions accessible from the home screen
Link the manifest in your HTML:
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2196F3">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
Handling the Installation Prompt
The beforeinstallprompt event fires when the browser detects an installable app. Capture and display it strategically:
// app.js
let deferredPrompt;
// Capture the beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (event) => {
// Prevent the mini-infobar from appearing
event.preventDefault();
// Store the event for later use
deferredPrompt = event;
// Show your custom install button
showInstallButton();
});
// Display install button
function showInstallButton() {
const installButton = document.getElementById('install-btn');
installButton.style.display = 'block';
installButton.addEventListener('click', async () => {
if (!deferredPrompt) {
return;
}
// Show the install prompt
deferredPrompt.prompt();
// Wait for user response
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted the install prompt');
installButton.style.display = 'none';
} else {
console.log('User dismissed the install prompt');
}
// Clear the deferred prompt
deferredPrompt = null;
});
}
// Listen for successful installation
window.addEventListener('appinstalled', () => {
console.log('PWA was installed');
// Hide install button
document.getElementById('install-btn').style.display = 'none';
// Track installation in analytics
trackEvent('app_installed');
});
// Check if app is already installed
function isAppInstalled() {
if (window.matchMedia('(display-mode: standalone)').matches) {
return true;
}
if (navigator.standalone === true) {
return true;
}
return false;
}
// Adjust UI based on installation status
if (isAppInstalled()) {
console.log('App is running in standalone mode');
// Hide install button, adjust UI for installed app
}
Installation Best Practices
Timing: Ask for installation after users have engaged with your app, not immediately on page load.
// Good: Ask after user completes an action
function onUserAction() {
// Track engagement
userEngagementCount++;
// Ask to install after 3 interactions
if (userEngagementCount === 3 && deferredPrompt) {
showInstallPrompt();
}
}
Messaging: Clearly communicate the value of installing your app.
<button id="install-btn" style="display: none;">
๐ฑ Install App - Access offline and get notifications
</button>
Feature 2: Notification Badges
Notification badges display a small indicator on your app icon showing unread content count. This keeps users informed without being intrusive.
Understanding the Badging API
The Badging API allows you to set a badge on your app icon:
// Set a badge with a number
navigator.setAppBadge(5);
// Set a badge without a number (just a dot)
navigator.setAppBadge();
// Clear the badge
navigator.clearAppBadge();
Implementing Badge Updates
Here’s a practical example that updates badges based on unread notifications:
// badge-manager.js
class BadgeManager {
constructor() {
this.unreadCount = 0;
this.init();
}
async init() {
// Check if Badging API is supported
if (!('setAppBadge' in navigator)) {
console.log('Badging API not supported');
return;
}
// Load initial unread count
this.unreadCount = await this.getUnreadCount();
this.updateBadge();
// Listen for new notifications
this.setupNotificationListener();
}
async getUnreadCount() {
try {
const response = await fetch('/api/unread-count');
const data = await response.json();
return data.count;
} catch (error) {
console.error('Failed to fetch unread count:', error);
return 0;
}
}
async updateBadge() {
if (!('setAppBadge' in navigator)) {
return;
}
try {
if (this.unreadCount > 0) {
await navigator.setAppBadge(this.unreadCount);
} else {
await navigator.clearAppBadge();
}
} catch (error) {
console.error('Failed to update badge:', error);
}
}
setupNotificationListener() {
// Listen for new messages via WebSocket or polling
const eventSource = new EventSource('/api/notifications');
eventSource.addEventListener('new-message', async (event) => {
this.unreadCount++;
await this.updateBadge();
// Show notification
this.showNotification(JSON.parse(event.data));
});
eventSource.addEventListener('message-read', async (event) => {
this.unreadCount = Math.max(0, this.unreadCount - 1);
await this.updateBadge();
});
}
async showNotification(message) {
if (!('serviceWorker' in navigator)) {
return;
}
const registration = await navigator.serviceWorker.ready;
registration.showNotification(message.title, {
body: message.body,
icon: '/images/icon-192x192.png',
badge: '/images/badge-72x72.png',
tag: 'notification',
data: message
});
}
async markAsRead() {
this.unreadCount = Math.max(0, this.unreadCount - 1);
await this.updateBadge();
}
}
// Initialize badge manager
const badgeManager = new BadgeManager();
Service Worker Badge Integration
Update badges when handling push notifications:
// service-worker.js
self.addEventListener('push', async (event) => {
const data = event.data.json();
// Show notification
await self.registration.showNotification(data.title, {
body: data.body,
icon: '/images/icon-192x192.png',
badge: '/images/badge-72x72.png'
});
// Update badge
if ('setAppBadge' in self.registration) {
const unreadCount = await getUnreadCount();
await self.registration.setAppBadge(unreadCount);
}
});
self.addEventListener('notificationclick', async (event) => {
event.notification.close();
// Clear badge when user opens the app
if ('clearAppBadge' in self.registration) {
await self.registration.clearAppBadge();
}
});
Badge Best Practices
Update frequency: Don’t update badges too frequently. Batch updates when possible.
// Debounce badge updates
let badgeUpdateTimeout;
function debouncedUpdateBadge() {
clearTimeout(badgeUpdateTimeout);
badgeUpdateTimeout = setTimeout(() => {
updateBadge();
}, 500);
}
Meaningful numbers: Only show badges when they provide value. Don’t badge everything.
// Only show badge for important items
async function updateBadge() {
const importantCount = await getImportantUnreadCount();
if (importantCount > 0) {
await navigator.setAppBadge(importantCount);
} else {
await navigator.clearAppBadge();
}
}
Feature 3: Native Sharing (Web Share API)
The Web Share API lets users share content through native sharing dialogs, just like native apps.
Understanding the Web Share API
The Web Share API provides access to the device’s native sharing mechanism:
// Basic share
await navigator.share({
title: 'Check this out',
text: 'This is awesome',
url: 'https://example.com'
});
// Share with files
await navigator.share({
files: [new File(['content'], 'file.txt', { type: 'text/plain' })]
});
Implementing Native Sharing
Here’s a practical implementation with fallbacks:
// share-manager.js
class ShareManager {
constructor() {
this.supportsWebShare = 'share' in navigator;
this.supportsWebShareFiles = this.supportsWebShare &&
'canShare' in navigator;
}
async shareContent(data) {
// Check if Web Share API is supported
if (!this.supportsWebShare) {
this.fallbackShare(data);
return;
}
try {
// Validate shareable data
if (this.supportsWebShareFiles && data.files) {
if (!navigator.canShare(data)) {
throw new Error('Cannot share this data');
}
}
await navigator.share(data);
console.log('Content shared successfully');
} catch (error) {
if (error.name === 'AbortError') {
console.log('User cancelled sharing');
} else {
console.error('Share failed:', error);
this.fallbackShare(data);
}
}
}
fallbackShare(data) {
// Fallback: Copy to clipboard or show share dialog
const shareText = `${data.title}\n${data.text}\n${data.url}`;
if ('clipboard' in navigator) {
navigator.clipboard.writeText(shareText).then(() => {
this.showToast('Copied to clipboard');
});
} else {
// Show custom share dialog
this.showCustomShareDialog(data);
}
}
showCustomShareDialog(data) {
const dialog = document.createElement('div');
dialog.className = 'share-dialog';
dialog.innerHTML = `
<div class="share-content">
<h2>Share</h2>
<p>${data.text}</p>
<div class="share-options">
<button onclick="copyToClipboard('${data.url}')">
๐ Copy Link
</button>
<button onclick="shareViaEmail('${data.title}', '${data.url}')">
โ๏ธ Email
</button>
</div>
</div>
`;
document.body.appendChild(dialog);
}
showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
}
// Usage
const shareManager = new ShareManager();
// Share button handler
document.getElementById('share-btn').addEventListener('click', async () => {
await shareManager.shareContent({
title: 'Check out this article',
text: 'I found this interesting article about web development',
url: window.location.href
});
});
Advanced Sharing with Files
Share files directly from your app:
async function shareScreenshot() {
// Capture canvas or screenshot
const canvas = document.getElementById('canvas');
canvas.toBlob(async (blob) => {
const file = new File([blob], 'screenshot.png', { type: 'image/png' });
if (navigator.canShare && navigator.canShare({ files: [file] })) {
try {
await navigator.share({
files: [file],
title: 'My Screenshot',
text: 'Check out this screenshot'
});
} catch (error) {
console.error('Share failed:', error);
}
}
});
}
Sharing Best Practices
Provide context: Include meaningful title and description.
// Good: Descriptive sharing
await navigator.share({
title: 'Article: "The Future of Web Development"',
text: 'An insightful article about emerging web technologies',
url: 'https://example.com/articles/future-of-web'
});
// Bad: Generic sharing
await navigator.share({
title: 'Check this out',
url: 'https://example.com'
});
Check support before showing UI: Only show share buttons if the API is available.
function setupShareButtons() {
const shareBtn = document.getElementById('share-btn');
if ('share' in navigator) {
shareBtn.style.display = 'block';
} else {
shareBtn.style.display = 'none';
}
}
Browser Support and Fallbacks
Compatibility Matrix
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Installation | โ | โ | โ ๏ธ | โ |
| Badging API | โ | โ | โ | โ |
| Web Share API | โ | โ | โ | โ |
Feature Detection Pattern
// Comprehensive feature detection
const features = {
installation: 'onbeforeinstallprompt' in window,
badging: 'setAppBadge' in navigator,
sharing: 'share' in navigator,
serviceWorker: 'serviceWorker' in navigator
};
// Use features conditionally
if (features.badging) {
initializeBadging();
}
if (features.sharing) {
showShareButton();
}
if (features.installation) {
setupInstallPrompt();
}
Security Considerations
HTTPS requirement: All PWA features require HTTPS (except localhost).
// Check for secure context
if (!window.isSecureContext) {
console.warn('PWA features require HTTPS');
}
Manifest validation: Ensure your manifest is valid and accessible.
# Validate manifest
curl -I https://example.com/manifest.json
# Should return 200 OK with Content-Type: application/manifest+json
Service Worker scope: Ensure Service Worker scope matches your manifest scope.
// service-worker.js registration
navigator.serviceWorker.register('/service-worker.js', {
scope: '/' // Must match manifest scope
});
Conclusion
These three PWA featuresโinstallation, badges, and native sharingโtransform web applications into experiences that rival native apps. By implementing them thoughtfully, you create:
- Better engagement: Users install your app and stay informed with badges
- Improved sharing: Native sharing dialogs make content distribution effortless
- Native-like experience: Your web app feels like a first-class citizen on users’ devices
Key takeaways:
- Installation: Use Web App Manifest and beforeinstallprompt to enable app installation
- Badges: Implement the Badging API to keep users informed about unread content
- Sharing: Leverage Web Share API for native sharing experiences
- Progressive enhancement: Always provide fallbacks for unsupported browsers
- User-centric: Ask for permissions at the right time and provide clear value
Start implementing these features today. Your users will appreciate the native-like experience, and your engagement metrics will reflect the improvement. The web is becoming more capable every dayโmake sure your applications take full advantage of these powerful APIs.
Comments