HTMLmediummeta
Implement favicons for all devices
rule · favicons
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
HTML
<!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
HTML
<!-- 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)
SVG
<!-- 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
SVG
<!-- 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
JavaScript
// 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
JSX
// 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)
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
<?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
JavaScript
// 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
JavaScript
// 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
JavaScript
// 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
JavaScript
// 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
JSON
{
"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
JavaScript
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.