Angular Favicon Guide: Configuration and Best Practices
Step-by-step guide to adding favicons in Angular, including angular.json asset configuration and per-environment icons.
Angular Favicons: Configuration Over Convention
Angular takes a different approach to favicons than its counterparts. While React says "just drop it in public," Angular says "declare it in your configuration." This might seem like extra work, but it offers precise control over asset handling, build optimisation, and multi-project workspaces.
A note on versions first, because it matters. This guide was originally written for Angular 18. Angular 18 is now well past end of life - Angular ships a major every year with 12 months active support and 12 months LTS, and only v20 and up are still supported. Current stable is Angular 22. The good news is that the favicon story has barely moved: the public/ folder that v18 introduced is still the default, and everything below applies from v18 straight through to v22. If you're on 18, the favicon isn't your problem - the upgrade is.
The Angular Way: angular.json Configuration
Basic Favicon Setup
Since v18, static assets live in a top-level public/ folder:
my-angular-app/
├── public/
│ └── favicon.ico
├── src/
│ └── index.html
├── angular.json
└── package.jsonThe default angular.json already copies that whole folder to the build output, so a fresh project needs no change at all:
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"assets": [
{ "glob": "**/*", "input": "public" }
]
}
}
}
}
}
}On projects generated before v18 you'll see the older shape instead - "src/favicon.ico" and "src/assets" listed individually. That still works. If you migrate to public/, move the files and replace both entries with the single glob above, or you'll ship the icons twice.
Reference it in src/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Angular App</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>Multiple Favicon Sizes and Types
For comprehensive device support:
Step 1: Add Favicon Files
public/
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
└── site.webmanifestStep 2: Update angular.json
Nothing to do here. The default { "glob": "**/*", "input": "public" } entry already picks up every file you just dropped in. That's the main thing public/ bought us - listing each icon by hand is over.
Step 3: Update index.html
<head>
<!-- Basic favicon -->
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- PNG favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.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">
</head>Asset Copying with Glob Patterns
If you'd rather keep the icons in their own folder than loose in public/, the assets array takes glob objects with an output path:
{
"assets": [
{ "glob": "**/*", "input": "public" },
{ "glob": "**/*", "input": "src/favicons", "output": "/" }
]
}The output is what matters: "/" flattens the folder back to the site root, so src/favicons/favicon.ico still serves at /favicon.ico and your index.html hrefs don't change. Leave output off and the files land in a favicons/ subfolder instead, which is a fine choice as long as the hrefs agree.
Dynamic Favicons in Angular
Service for Favicon Management
// favicon.service.ts
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Injectable({
providedIn: 'root'
})
export class FaviconService {
constructor(@Inject(DOCUMENT) private document: Document) {}
setFavicon(href: string): void {
const link: HTMLLinkElement = this.document.querySelector("link[rel*='icon']") ||
this.document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = href;
this.document.head.appendChild(link);
}
setMultipleFavicons(favicons: { href: string; sizes?: string; type?: string }[]): void {
// Remove existing favicons
const existingLinks = this.document.querySelectorAll("link[rel*='icon']");
existingLinks.forEach(link => link.remove());
// Add new favicons
favicons.forEach(favicon => {
const link = this.document.createElement('link');
link.rel = 'icon';
link.href = favicon.href;
if (favicon.type) link.type = favicon.type;
if (favicon.sizes) link.sizes = favicon.sizes;
this.document.head.appendChild(link);
});
}
}Usage in Components
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { FaviconService } from './services/favicon.service';
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>'
})
export class AppComponent implements OnInit {
constructor(private faviconService: FaviconService) {}
ngOnInit(): void {
// Set favicon based on environment
const favicon = environment.production ? 'favicon.ico' : 'favicon-dev.ico';
this.faviconService.setFavicon(favicon);
}
}Notification Badge Example
// notification.service.ts
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(private faviconService: FaviconService) {
this.canvas = document.createElement('canvas');
this.canvas.width = 32;
this.canvas.height = 32;
this.ctx = this.canvas.getContext('2d')!;
}
updateFaviconBadge(count: number): void {
const img = new Image();
img.onload = () => {
// Clear canvas
this.ctx.clearRect(0, 0, 32, 32);
// Draw original favicon
this.ctx.drawImage(img, 0, 0, 32, 32);
if (count > 0) {
// Draw red circle
this.ctx.fillStyle = '#ff0000';
this.ctx.beginPath();
this.ctx.arc(24, 8, 8, 0, 2 * Math.PI);
this.ctx.fill();
// Draw count
this.ctx.fillStyle = '#ffffff';
this.ctx.font = 'bold 11px Arial';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(count > 99 ? '99+' : count.toString(), 24, 8);
}
// Update favicon
this.faviconService.setFavicon(this.canvas.toDataURL('image/png'));
};
img.src = 'favicon-32x32.png';
}
}PWA Configuration
For Progressive Web App support:
manifest.webmanifest
{
"name": "My Angular App",
"short_name": "AngularApp",
"theme_color": "#1976d2",
"background_color": "#fafafa",
"display": "standalone",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Angular Service Worker
ng add @angular/pwaThis automatically:
Environment-Specific Favicons
- Adds service worker support
- Creates
manifest.webmanifest(note the extension - it is notmanifest.json, and a<link rel="manifest" href="manifest.json">written from memory will 404) - Updates index.html with the manifest link and PWA meta tags
- Configures icon assets
Different favicons for dev/staging/production:
There are two ways to do this, and the one most tutorials reach for doesn't work. Let's take the broken one first, because you'll meet it.
What Doesn't Work: fileReplacements
{
"fileReplacements": [
{ "replace": "src/favicon-dev.ico", "with": "src/favicon.ico" }
]
}This looks right and does nothing. fileReplacements swaps files in the TypeScript program - that's the whole scope of the feature, and Angular's docs say so plainly. A .ico is never part of the TypeScript program, so the build walks straight past the rule. No error, no warning, wrong favicon in production. Use fileReplacements for environment.ts and nothing else.
What Does Work: Per-Configuration Assets
Point each build configuration at a different source folder. Keep one set of icons per environment and let the assets array choose:
{
"configurations": {
"production": {
"assets": [{ "glob": "**/*", "input": "public/prod", "output": "/" }]
},
"development": {
"assets": [{ "glob": "**/*", "input": "public/dev", "output": "/" }]
}
}
}Both folders hold a favicon.ico under the same name, output: "/" flattens them to the root, and index.html never changes.
The Runtime Alternative
If the icon depends on something only known at runtime, set it from the environment file instead and let the service below do the swap:
// environments/environment.ts - the default, used by the production build
export const environment = {
production: true,
faviconPath: 'favicon.ico'
};
// environments/environment.development.ts - swapped in by the development config
export const environment = {
production: false,
faviconPath: 'favicon-dev.ico'
};Note the direction. The modern CLI treats environment.ts as the default and replaces it with environment.development.ts in the development configuration, which is the reverse of the old environment.prod.ts arrangement.
The Application Builder
@angular-devkit/build-angular:application became the default in v18 and is what every current version uses. Here's the shape of the config, with the public assets entry in context:
{
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/my-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public",
"output": "/"
}
]
}
}
}
}Testing Favicon Implementation
Development Server
ng serve
# Check: http://localhost:4200/favicon.icoProduction Build
ng build
# Check dist folder for favicon filesE2E Testing
Angular ships no first-party e2e runner any more, so pick one - Playwright and Cypress are both supported by ng e2e. Here it is in Playwright:
// e2e/favicon.spec.ts
import { test, expect } from '@playwright/test';
test('serves a favicon link that resolves', async ({ page, request }) => {
await page.goto('/');
const href = await page.locator('link[rel="icon"]').first().getAttribute('href');
expect(href).toContain('favicon.ico');
// The link existing is not the same as the file shipping.
const response = await request.get(new URL(href!, page.url()).toString());
expect(response.status()).toBe(200);
});That second assertion is the one that earns its keep. A <link> in index.html proves nothing about whether the asset actually made it into dist - a missing assets entry gives you a perfectly valid tag pointing at a 404.
If you've inherited a suite that still imports protractor, that's your signal to rewrite it. Protractor was deprecated in 2022, reached end of life in September 2023, and the Angular CLI dropped its builder entirely in v19. It will not run on a current Angular.
Common Issues and Solutions
Favicon Not Updating?
- 1Clear Angular cache:
```bash
rm -rf .angular/cache
ng serve
```
- 1Check output path:
Ensure favicon is copied to dist folder
- 1Browser cache:
Add version query: href="favicon.ico?v=2"
Works Locally, Not in Production?
Check base href configuration:
<!-- For subdirectory deployment -->
<base href="/my-app/">Or build with base href:
ng build --base-href /my-app/Creating Favicons for Angular
Need all these favicon sizes? Use Unwrite's Favicon Generator. Upload your logo and get a complete favicon package ready for Angular, including all sizes and a web app manifest. Everything processes privately in your browser. Drop the lot into public/ and you're done.
Angular Favicon Checklist
- [ ] Add favicon.ico to the public folder
- [ ] Confirm angular.json copies public (a fresh project already does)
- [ ] Include multiple sizes for devices
- [ ] Add apple-touch-icon for iOS
- [ ] Create site.webmanifest for PWA
- [ ] Test in a production build, and check the file is really in dist
- [ ] Use per-configuration assets for environment-specific icons, never fileReplacements
- [ ] Implement FaviconService for dynamic needs
The Angular approach might require more configuration than dropping a file in a folder, but it provides the control and consistency that large applications need. Embrace the configuration - it's there to help you build better apps.