Implement favicons for all devices
All necessary favicon formats are implemented for browsers, devices, and PWA support.
- Minimum: favicon.ico (32x32) + apple-touch-icon.png (180x180)
- Modern: SVG favicon with dark mode support
- PWA: Include in manifest.json with multiple sizes
- Use RealFaviconGenerator or favicons npm package
Rule Details
Proper favicon implementation ensures consistent brand representation across browsers, devices, and platforms with optimized formats and sizes for different use cases.
Code Examples
Complete Favicon Set
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Complete Favicon Implementation</title>
<!-- Modern browsers - SVG favicon (scalable) -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<!-- Fallback PNG favicon for browsers without SVG support -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<!-- Legacy ICO fallback (placed in root directory) -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png">
<!-- Android Chrome Icons -->
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/android-chrome-512x512.png">
<!-- Web App Manifest -->
<link rel="manifest" href="/site.webmanifest">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#2d89ef">
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
<meta name="msapplication-config" content="/browserconfig.xml">
<!-- Theme colors -->
<meta name="theme-color" content="#ffffff">
<meta name="msapplication-navbutton-color" content="#ffffff">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
</head>
<body>
<!-- Page content -->
</body>
</html>Minimal Modern Implementation
<!-- Minimal setup for modern browsers -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/manifest.json">Why It Matters
Missing or low-quality favicons make sites look unprofessional in browser tabs, bookmarks, and home screens—damaging brand recognition and user trust.
SVG Favicon (Recommended)
<!-- favicon.svg - Modern scalable favicon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Dark mode support -->
<style>
@media (prefers-color-scheme: dark) {
.logo-bg { fill: #ffffff; }
.logo-text { fill: #000000; }
}
@media (prefers-color-scheme: light) {
.logo-bg { fill: #000000; }
.logo-text { fill: #ffffff; }
}
</style>
<!-- Logo design -->
<rect class="logo-bg" width="32" height="32" rx="6"/>
<text class="logo-text" x="16" y="20" font-family="Arial, sans-serif"
font-size="18" font-weight="bold" text-anchor="middle">M</text>
</svg>Animated SVG Favicon
<!-- animated-favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<style>
.spinner {
animation: spin 2s linear infinite;
transform-origin: 16px 16px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
<circle cx="16" cy="16" r="14" fill="none" stroke="#4F46E5" stroke-width="4"/>
<circle class="spinner" cx="16" cy="4" r="3" fill="#4F46E5"/>
</svg>Dynamic Favicon Implementation
JavaScript Favicon Controller
// favicon-controller.js
class FaviconController {
constructor() {
this.defaultFavicon = '/favicon.svg'
this.favicons = {
default: '/favicon.svg',
notification: '/favicon-notification.svg',
error: '/favicon-error.svg',
success: '/favicon-success.svg',
loading: '/favicon-loading.svg'
}
this.currentState = 'default'
}
setFavicon(state = 'default') {
if (this.currentState === state) return
const faviconUrl = this.favicons[state] || this.favicons.default
this.updateFaviconLink(faviconUrl)
this.currentState = state
}
updateFaviconLink(href) {
// Remove existing favicon links
const existingFavicons = document.querySelectorAll('link[rel*="icon"]')
existingFavicons.forEach(link => {
if (link.getAttribute('rel').includes('icon') &&
!link.getAttribute('rel').includes('apple')) {
link.remove()
}
})
// Add new favicon
const link = document.createElement('link')
link.rel = 'icon'
link.type = 'image/svg+xml'
link.href = href
document.head.appendChild(link)
}
showNotification() {
this.setFavicon('notification')
}
showError() {
this.setFavicon('error')
}
showSuccess() {
this.setFavicon('success')
// Reset to default after 3 seconds
setTimeout(() => {
this.setFavicon('default')
}, 3000)
}
showLoading() {
this.setFavicon('loading')
}
reset() {
this.setFavicon('default')
}
// Badge functionality for notification count
generateBadgeFavicon(count, baseIcon = this.favicons.default) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const size = 32
canvas.width = size
canvas.height = size
// Load base icon
const img = new Image()
img.onload = () => {
// Draw base icon
ctx.drawImage(img, 0, 0, size, size)
if (count > 0) {
// Draw notification badge
const badgeSize = size * 0.6
const badgeX = size - badgeSize
const badgeY = 0
// Badge background
ctx.fillStyle = '#ff4444'
ctx.beginPath()
ctx.arc(badgeX + badgeSize/2, badgeY + badgeSize/2, badgeSize/2, 0, 2 * Math.PI)
ctx.fill()
// Badge text
ctx.fillStyle = 'white'
ctx.font = `${badgeSize * 0.6}px Arial`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const text = count > 99 ? '99+' : count.toString()
ctx.fillText(text, badgeX + badgeSize/2, badgeY + badgeSize/2)
}
// Convert to data URL
const dataUrl = canvas.toDataURL('image/png')
resolve(dataUrl)
}
img.src = baseIcon
})
}
async showBadge(count) {
const badgedFavicon = await this.generateBadgeFavicon(count)
this.updateFaviconLink(badgedFavicon)
}
}
// Usage examples
const faviconController = new FaviconController()
// Show loading state
faviconController.showLoading()
// Show success after operation
setTimeout(() => {
faviconController.showSuccess()
}, 2000)
// Show notification badge
faviconController.showBadge(5)
// Reset to default
faviconController.reset()React Favicon Hook
// hooks/useFavicon.js
import { useEffect, useRef } from 'react'
export function useFavicon(href, type = 'image/svg+xml') {
const prevHref = useRef(href)
useEffect(() => {
if (prevHref.current === href) return
const updateFavicon = () => {
// Remove existing favicon
const existingFavicons = document.querySelectorAll('link[rel*="icon"]:not([rel*="apple"])')
existingFavicons.forEach(link => link.remove())
// Add new favicon
const link = document.createElement('link')
link.rel = 'icon'
link.type = type
link.href = href
document.head.appendChild(link)
prevHref.current = href
}
updateFavicon()
}, [href, type])
}
// components/FaviconProvider.jsx
import { createContext, useContext, useCallback, useState } from 'react'
import { useFavicon } from '../hooks/useFavicon'
const FaviconContext = createContext()
const faviconStates = {
default: '/favicon.svg',
loading: '/favicon-loading.svg',
error: '/favicon-error.svg',
success: '/favicon-success.svg',
notification: '/favicon-notification.svg'
}
export function FaviconProvider({ children }) {
const [currentState, setCurrentState] = useState('default')
const [notificationCount, setNotificationCount] = useState(0)
useFavicon(faviconStates[currentState])
const setFaviconState = useCallback((state) => {
if (faviconStates[state]) {
setCurrentState(state)
}
}, [])
const showNotification = useCallback((count = 1) => {
setNotificationCount(count)
setCurrentState('notification')
}, [])
const clearNotification = useCallback(() => {
setNotificationCount(0)
setCurrentState('default')
}, [])
const value = {
setFaviconState,
showNotification,
clearNotification,
currentState,
notificationCount
}
return (
<FaviconContext.Provider value={value}>
{children}
</FaviconContext.Provider>
)
}
export const useFaviconContext = () => {
const context = useContext(FaviconContext)
if (!context) {
throw new Error('useFaviconContext must be used within FaviconProvider')
}
return context
}
// Usage in components
function NotificationButton() {
const { showNotification, clearNotification, notificationCount } = useFaviconContext()
return (
<div>
<button onClick={() => showNotification(notificationCount + 1)}>
Add Notification ({notificationCount})
</button>
<button onClick={clearNotification}>
Clear Notifications
</button>
</div>
)
}Configuration Files
Web App Manifest (manifest.json)
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
],
"categories": ["productivity", "utilities"],
"lang": "en-US",
"dir": "ltr"
}Browser Config (browserconfig.xml)
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/mstile-70x70.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<wide310x150logo src="/mstile-310x150.png"/>
<TileColor>#2d89ef</TileColor>
</tile>
</msapplication>
</browserconfig>Framework Integration
Next.js Favicon Setup
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/favicon.ico',
headers: [
{
key: 'Cache-Control',
value: 'public, immutable, max-age=31536000'
}
]
}
]
}
}
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head>
{/* Favicon */}
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
// components/Head.js - Reusable head component
import Head from 'next/head'
export default function CustomHead({
title = 'My App',
description = 'My awesome app',
favicon = '/favicon.svg'
}) {
return (
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href={favicon} />
</Head>
)
}Gatsby Favicon Plugin
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `My Gatsby Site`,
short_name: `MyGatsby`,
start_url: `/`,
background_color: `#ffffff`,
theme_color: `#000000`,
display: `minimal-ui`,
icon: `src/images/icon.png`, // This generates all favicon sizes
},
},
// Alternative manual setup
{
resolve: `gatsby-plugin-favicon`,
options: {
logo: "./src/images/favicon.png",
// WebApp Manifest
appName: 'My Gatsby Site',
appDescription: 'My awesome Gatsby site',
developerName: null,
developerURL: null,
dir: 'auto',
lang: 'en-US',
background: '#fff',
theme_color: '#000',
display: 'standalone',
orientation: 'any',
start_url: '/',
version: '1.0',
icons: {
android: true,
appleIcon: true,
appleStartup: true,
coast: false,
favicons: true,
firefox: true,
yandex: false,
windows: false
}
}
}
]
}Vue.js with Vite
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
manifest: {
name: 'My Vue App',
short_name: 'MyVue',
description: 'My awesome Vue app',
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: '/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: '/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
})
]
})Automated Favicon Generation
Build Script for Favicon Generation
// scripts/generate-favicons.js
const sharp = require('sharp')
const fs = require('fs').promises
const path = require('path')
class FaviconGenerator {
constructor(sourceImage, outputDir = './public') {
this.sourceImage = sourceImage
this.outputDir = outputDir
this.sizes = {
'favicon-16x16.png': 16,
'favicon-32x32.png': 32,
'apple-touch-icon.png': 180,
'apple-touch-icon-152x152.png': 152,
'apple-touch-icon-120x120.png': 120,
'apple-touch-icon-76x76.png': 76,
'android-chrome-192x192.png': 192,
'android-chrome-512x512.png': 512,
'mstile-70x70.png': 70,
'mstile-144x144.png': 144,
'mstile-150x150.png': 150,
'mstile-310x310.png': 310
}
}
async generateAll() {
try {
// Ensure output directory exists
await fs.mkdir(this.outputDir, { recursive: true })
// Generate PNG favicons
for (const [filename, size] of Object.entries(this.sizes)) {
await this.generatePng(filename, size)
}
// Generate ICO file
await this.generateIco()
// Generate manifest and browserconfig
await this.generateManifest()
await this.generateBrowserConfig()
console.log('✅ All favicons generated successfully')
} catch (error) {
console.error('❌ Favicon generation failed:', error)
throw error
}
}
async generatePng(filename, size) {
const outputPath = path.join(this.outputDir, filename)
await sharp(this.sourceImage)
.resize(size, size, {
kernel: sharp.kernel.lanczos3,
fit: 'cover',
position: 'center'
})
.png({
quality: 90,
compressionLevel: 9
})
.toFile(outputPath)
console.log(`Generated: ${filename} (${size}x${size})`)
}
async generateIco() {
// Generate multiple sizes for ICO
const icoSizes = [16, 32, 48]
const icoBuffers = []
for (const size of icoSizes) {
const buffer = await sharp(this.sourceImage)
.resize(size, size)
.png()
.toBuffer()
icoBuffers.push(buffer)
}
// Use a library like 'to-ico' for proper ICO generation
const toIco = require('to-ico')
const icoBuffer = await toIco(icoBuffers)
await fs.writeFile(path.join(this.outputDir, 'favicon.ico'), icoBuffer)
console.log('Generated: favicon.ico')
}
async generateManifest() {
const manifest = {
name: "My Application",
short_name: "MyApp",
icons: [
{
src: "/android-chrome-192x192.png",
sizes: "192x192",
type: "image/png"
},
{
src: "/android-chrome-512x512.png",
sizes: "512x512",
type: "image/png"
}
],
theme_color: "#ffffff",
background_color: "#ffffff",
display: "standalone"
}
await fs.writeFile(
path.join(this.outputDir, 'site.webmanifest'),
JSON.stringify(manifest, null, 2)
)
console.log('Generated: site.webmanifest')
}
async generateBrowserConfig() {
const browserconfig = `<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/mstile-70x70.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<TileColor>#2d89ef</TileColor>
</tile>
</msapplication>
</browserconfig>`
await fs.writeFile(
path.join(this.outputDir, 'browserconfig.xml'),
browserconfig
)
console.log('Generated: browserconfig.xml')
}
generateHtml() {
return `<!-- Favicon HTML -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="msapplication-TileColor" content="#2d89ef">
<meta name="msapplication-config" content="/browserconfig.xml">
<meta name="theme-color" content="#ffffff">`
}
}
// Usage
async function main() {
if (process.argv.length < 3) {
console.error('Usage: node generate-favicons.js <source-image>')
process.exit(1)
}
const sourceImage = process.argv[2]
const generator = new FaviconGenerator(sourceImage)
await generator.generateAll()
console.log('\n' + generator.generateHtml())
}
if (require.main === module) {
main().catch(console.error)
}
module.exports = FaviconGeneratorPackage.json Scripts
{
"scripts": {
"favicon:generate": "node scripts/generate-favicons.js src/images/logo.png",
"favicon:optimize": "imagemin public/*.png --out-dir=public/",
"favicon:validate": "node scripts/validate-favicons.js"
},
"devDependencies": {
"sharp": "^0.32.0",
"to-ico": "^2.1.6",
"imagemin": "^8.0.1",
"imagemin-pngquant": "^9.0.2"
}
}Best Practices
- Use SVG: Modern browsers support SVG favicons with dark mode support
- Provide Fallbacks: Include PNG and ICO formats for older browsers
- Optimize Sizes: Generate all required sizes for different platforms
- Proper Caching: Set long cache headers for favicon files
- Test Across Devices: Verify display on different browsers and devices
- Consistent Branding: Ensure favicon matches your brand identity
- Performance: Keep file sizes small, especially for mobile
- Progressive Enhancement: Use modern features with graceful fallbacks
- Automation: Automate generation and validation in build process
- Regular Updates: Update favicons when branding changes
Size Requirements
Standard Sizes
- 16x16px: Browser tabs, bookmarks
- 32x32px: Desktop shortcuts, taskbar
- 48x48px: Windows desktop shortcuts
- 180x180px: Apple touch icon (iOS Safari)
- 192x192px: Android Chrome home screen
- 512x512px: Android Chrome splash screen
Comprehensive Size List
const faviconSizes = {
// Standard web favicons
16: 'favicon-16x16.png',
32: 'favicon-32x32.png',
48: 'favicon-48x48.png',
// Apple Touch Icons
57: 'apple-touch-icon-57x57.png',
60: 'apple-touch-icon-60x60.png',
72: 'apple-touch-icon-72x72.png',
76: 'apple-touch-icon-76x76.png',
114: 'apple-touch-icon-114x114.png',
120: 'apple-touch-icon-120x120.png',
144: 'apple-touch-icon-144x144.png',
152: 'apple-touch-icon-152x152.png',
180: 'apple-touch-icon-180x180.png',
// Android Chrome
192: 'android-chrome-192x192.png',
512: 'android-chrome-512x512.png',
// Microsoft Tiles
70: 'mstile-70x70.png',
144: 'mstile-144x144.png',
150: 'mstile-150x150.png',
310: 'mstile-310x310.png'
}Remember: While comprehensive favicon sets provide the best coverage, a minimal modern implementation with SVG and PNG fallbacks is sufficient for most modern applications.
Support Notes
- Favicon and icon behavior differs across platforms, pinned-tab contexts, and install surfaces, so verify the final browser and OS icon outputs rather than relying only on one file.
- Document any platform-specific icon fallbacks when the full manifest or maskable icon path is not available.
Verification
Automated Checks
- Validate the rendered HTML or linked assets with browser tooling, validators, or another automated check against representative live routes.
Manual Checks
- Verify the rendered browser behavior manually on representative routes and supported browsers so the user-facing outcome matches the rule.
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Verify that all necessary favicon formats and sizes are implemented correctly for optimal display across different browsers, devices, and platforms.
Fix
Auto-fix issues
Generate and implement a complete favicon set including ICO, PNG, SVG, and mobile/PWA icons with proper HTML declarations.
Explain
Learn more
Explain how proper favicon implementation improves brand recognition, user experience, and ensures consistent display across all browsers and devices.
Review
Code review
Review templates, server-rendered HTML, and shared components that output markup related to Implement favicons for all devices. Flag exact elements, attributes, and routes where the rendered HTML violates the rule.