Skip to main content
โšก Calmops

Building Native-Like Web Apps: Installation, Badges, and Sharing with PWA APIs

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:

  1. Web App Manifest: Describes your app (name, icons, colors, start URL)
  2. Service Worker: Enables offline functionality and background features
  3. beforeinstallprompt event: Triggered when the browser detects an installable app
  4. 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: standalone makes 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: maskable icons 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:

  1. Installation: Use Web App Manifest and beforeinstallprompt to enable app installation
  2. Badges: Implement the Badging API to keep users informed about unread content
  3. Sharing: Leverage Web Share API for native sharing experiences
  4. Progressive enhancement: Always provide fallbacks for unsupported browsers
  5. 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