DEV Community

Sashikumar Yadav
Sashikumar Yadav

Posted on

Centralizing SVG Handling in Angular Applications

SVG icons are essential for modern web UIs, but managing them directly in templates becomes challenging as applications grow. This post demonstrates how we implemented a scalable approach to SVG management in DevsWhoRun Angular application using modern directives and TypeScript.

The Problem

Inline SVGs in templates lead to several issues:

<button><svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438..." fill="currentColor"></path></svg>
  Sign in with GitHub
</button>
Enter fullscreen mode Exit fullscreen mode
  • Template Bloat: SVG markup clutters templates
  • Duplication: Same SVGs repeated across components
  • Inconsistency: Inconsistent rendering
  • Maintenance: Updates require changes in multiple files

The Solution: A Centralized SVG Approach

Our solution consists of two key components:

  1. A TypeScript constants file for SVG definitions
  2. An Angular directive for dynamic SVG rendering

Step 1: Create the SVG Constants File

// svg-icon-constants.ts
import { Icon } from "./types";

// Type-safe icon name constants
export const ICON_NAME = {
  google: 'google',
  github: 'github',
  sun: 'sun',
  moon: 'moon',
  smile: 'smile',
  discord: 'discord'
}

// SVG path data mapped to icon names
export const SVG_ICONS: { [key: Icon]: string } = {
  [ICON_NAME.google]: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
    <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"></path>
  </svg>`,

  [ICON_NAME.github]: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
    <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" fill="currentColor"></path>
  </svg>`
  // Additional icons omitted for brevity
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the SVG Icon Directive

// svg-icon.ts
import { Directive, ElementRef, inject, input, OnInit, Renderer2 } from '@angular/core';
import { SVG_ICONS } from './svg-icon-constants';
import { Icon } from './types';

@Directive({
  selector: '[appSvgIcon]',
})
export class SvgIcon implements OnInit {
  // Modern Angular signals-based inputs
  iconName = input<Icon>('google');
  iconClass = input<string>('');
  fill = input<string>('currentColor');

  // Dependency injection using inject function
  private readonly el = inject(ElementRef);
  private readonly renderer = inject(Renderer2);

  ngOnInit(): void {
    if (!this.iconName() || !SVG_ICONS[this.iconName()]) {
      console.error(`SVG icon not found: ${this.iconName()}`);
      return;
    }

    const svgString = SVG_ICONS[this.iconName()];
    const parser = new DOMParser();
    const doc = parser.parseFromString(svgString, 'image/svg+xml');
    const svgElement = doc.documentElement;

    // Add CSS classes if provided
    if (this.iconClass()) {const classes = this.iconClass().split(' ');
      classes.forEach(className => {
        if (className) {
          this.renderer.addClass(svgElement, className);
        }
      });
    }

    // Set fill color for all paths
    const paths = svgElement.querySelectorAll('path');
    paths.forEach(path => {
      path.setAttribute('fill', this.fill());
    });

    // Append the SVG to the host element
    this.renderer.setProperty(this.el.nativeElement, 'innerHTML','');
    this.renderer.appendChild(this.el.nativeElement, svgElement);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Type Definitions

// types.ts
import { ICON_NAME } from "./svg-icon-constants";

// Creates a union type of all icon name values
export type Icon = (typeof ICON_NAME)[keyof typeof ICON_NAME];
Enter fullscreen mode Exit fullscreen mode

Step 4: Using the SVG Directive in Components

Implement the directive in any component:

// footer.ts
import { Component } from '@angular/core';
import { SvgIcon } from '../../../shared/directives/svg/svg-icon';
import { ICON_NAME } from '../../../shared/directives/svg/svg-icon-constants';

@Component({
  selector: 'app-footer',
  imports: [SvgIcon],
  template: `
    <footer class="bg-gray-800 text-white py-8">
      <div class="container mx-auto">
        <div class="flex justify-center space-x-6">
          <a href="https://github.com/yshashi/devswhomove" target="_blank" rel="noopener noreferrer">
            <span appSvgIcon [iconName]="iconName.github" iconClass="w-6 h-6"></span>
          </a>
          <a href="https://discord.gg/devswhomove" target="_blank" rel="noopener noreferrer">
            <span appSvgIcon [iconName]="iconName.discord" iconClass="w-6 h-6"></span>
          </a>
        </div>
        <p class="text-center mt-4">© 2025 DevsWhoMove. All rights reserved.</p>
      </div>
    </footer>
  `,
  standalone: true
})
export class Footer {
  protected readonly iconName = ICON_NAME;
}
Enter fullscreen mode Exit fullscreen mode

Technical Benefits

1. Clean, Type-Safe Templates

Before:

<a href="https://github.com/yshashi/devswhomove" target="_blank">
   <svg class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 .297c-6.63 0-12 5.373-    12 12 0 5.303 3.438 9.8 8.205 11.385..." fill="currentColor"></path>
   </svg>
</a>
Enter fullscreen mode Exit fullscreen mode

After:

<a href="https://github.com/yshashi/devswhomove" target="_blank">
   <span appSvgIcon [iconName]="iconName.github" iconClass="w-6 h-6"></span>
</a>
Enter fullscreen mode Exit fullscreen mode

2. Developer Experience Improvements

  • Type Safety: TypeScript interfaces ensure correct icon names
  • Centralized Management: Single source of truth for all SVGs
  • IDE Support: Autocomplete for icon names and properties
  • Consistent Styling: Apply classes uniformly across all icons

3. Performance Optimizations

  • Reduced Bundle Size: No duplicate SVG definitions
  • Efficient DOM Operations: Directive handles optimal rendering
  • Caching: Browser can cache SVG content

Advanced Implementation Options

1. Server-Side Rendering Support

For Angular Universal applications, ensure the directive works with SSR by checking the platform before performing DOM operations:

import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

// Modern dependency injection
private readonly platformId = inject(PLATFORM_ID);

ngOnInit(): void {
  // Only perform DOM operations in browser environment
  if (isPlatformBrowser(this.platformId)) {
     // SVG parsing and DOM manipulation code
     const svgString = SVG_ICONS[this.iconName()];
     const parser = new DOMParser();
     // Rest of the implementation...
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Lazy Loading Icons by Feature

For applications with many icons, implement lazy loading by feature using modern Angular patterns:

// feature-icons.ts
import { InjectionToken } from '@angular/core';

// Create a token for feature-specific icons
export const FEATURE_ICONS = new InjectionToken<Record<string, string>>('FEATURE_ICONS');

// Define feature-specific icons
export const DASHBOARD_ICONS = {
  chart: `<svg viewBox="0 0 24 24">...</svg>`,
  analytics: `<svg viewBox="0 0 24 24">...</svg>`
};

// In your feature module or component providers
providers: [
  {
    provide: FEATURE_ICONS,
    useValue: DASHBOARD_ICONS
  }
]

// Inject in directive
const featureIcons = inject(FEATURE_ICONS, { optional: true });

// Merge with core icons
const allIcons = { ...SVG_ICONS, ...featureIcons };
Enter fullscreen mode Exit fullscreen mode

Conclusion

Our centralized SVG handling approach in the DevsWhoRun Angular application demonstrates how modern Angular features like signals and functional dependency injection can create a clean, maintainable solution. By centralizing SVG definitions and using a directive for rendering, we've achieved:

  1. Improved Developer Experience: Type-safe icon references with IDE autocompletion
  2. Better Performance: Reduced bundle size and optimized rendering
  3. Enhanced Maintainability: Single source of truth for all SVG assets
  4. Flexible Styling: Dynamic class application and fill color control

This pattern scales well from small applications to enterprise-level projects, providing a solid foundation for consistent icon usage across your Angular applications.

Top comments (0)