Dynamic SVG Loading Mechanism in Vue-Color-Avatar: A Technical Deep Dive

Vue-Color-Avatar leverages JavaScript's dynamic import() alongside Vite's ?raw query parameter to lazy-load SVG avatar components on demand, reducing initial bundle size while enabling runtime color customization.

The vue-color-avatar library generates customizable avatars by compositing individual SVG widgets—such as faces, ears, and clothing—into a single cohesive image. Understanding its dynamic SVG loading mechanism reveals how modern Vite-based applications can efficiently manage large static asset libraries without bloating the initial JavaScript payload.

Registry of Lazy Imports in dynamic-data.ts

The foundation of the system lies in src/utils/dynamic-data.ts, which exports a centralized registry called widgetData. This object maps each WidgetType and shape combination to a function that returns a dynamic import promise.

// src/utils/dynamic-data.ts
type Data = Readonly<{
  [key in `${WidgetType}`]: { [key in string]: () => Promise<any> }
}>;

const widgetData: Data = {
  [WidgetType.Face]: {
    [FaceShape.Base]: () => import(`../assets/widgets/face/base.svg?raw`),
  },
  [WidgetType.Ear]: {
    [EarShape.Attached]: () => import(`../assets/widgets/ear/attached.svg?raw`),
    [EarShape.Detached]: () => import(`../assets/widgets/ear/detached.svg?raw`),
  },
  // Additional widget types (Eyes, Beard, Clothes, etc.) follow the same pattern
};

Each entry uses a lazy-import function that executes only when called. The ?raw suffix instructs Vite to treat the SVG file as a plain text string rather than a processed component, returning the raw markup via the module's default export.

Runtime Assembly in VueColorAvatar.vue

The component src/components/VueColorAvatar.vue orchestrates the loading process. Inside a watchEffect, the component reacts to changes in avatar options and triggers the dynamic resolution sequence.

Sorting by zIndex

First, the component sorts active widgets according to their zIndex values to ensure proper layering:

const sortedList = Object.entries(avatarOption.value.widgets).sort(
  ([, prev], [, next]) => {
    const ix = prev.zIndex ?? AVATAR_LAYER[prev.shape]?.zIndex ?? 0;
    const iix = next.zIndex ?? AVATAR_LAYER[next.shape]?.zIndex ?? 0;
    return ix - iix;
  }
);

Constructing Import Promises

Then it constructs an array of promises that invoke the registry functions:

const promises = sortedList.map(async ([widgetType, opt]) => {
  if (opt.shape !== NONE && widgetData?.[widgetType]?.[opt.shape]) {
    return (await widgetData[widgetType][opt.shape]()).default;
  }
  return '';
});

The check against NONE prevents loading when a widget type is disabled.

SVG Processing and Color Injection

Once resolved, the raw SVG strings undergo processing to inject dynamic colors and remove redundant root elements. The Promise.all resolution handles placeholder substitution and markup extraction:

const svgRawList = await Promise.all(promises).then((raw) => {
  return raw.map((svgRaw, i) => {
    const [widgetType, widget] = sortedList[i];
    let fill = widget.fillColor;
    
    /* Apply skin-color inheritance for ears */
    if (widgetType === WidgetType.Face) skinColor = fill;
    if (skinColor && widgetType === WidgetType.Ear) fill = skinColor;

    const content = svgRaw
      .slice(svgRaw.indexOf('>', svgRaw.indexOf('<svg')) + 1)
      .replace('</svg>', '')
      .replaceAll('$fillColor', fill || 'transparent');

    return `<g id="vue-color-avatar-${sortedList[i][0]}">${content}</g>`;
  });
});

The code strips the outer <svg> tags from each component (keeping only the inner content), replaces $fillColor placeholders with actual hex values, and wraps each part in a <g> element with a unique ID.

Final Composition and Rendering

The processed fragments are assembled into a single parent SVG structure:

svgContent.value = `
  <svg width="${avatarSize.value}" height="${avatarSize.value}"
       viewBox="0 0 ${avatarSize.value / 0.7} ${avatarSize.value / 0.7}"
       preserveAspectRatio="xMidYMax meet"
       fill="none" xmlns="http://www.w3.org/2000/svg">
    <g transform="translate(100, 65)">
      ${svgRawList.join('')}
    </g>
  </svg>`;

This final string is rendered via v-html, resulting in a composite avatar containing only the specific widgets selected by the user.

Why This Architecture Works

The dynamic SVG loading mechanism provides three key advantages:

  • Code-splitting: Vite generates separate chunks for each SVG file. The browser fetches only chunks corresponding to active avatar shapes, significantly reducing initial load time.
  • Raw string access: The ?raw query parameter enables string manipulation before DOM insertion, allowing runtime color substitution via replaceAll operations.
  • Type-safe registry: The widgetData structure uses TypeScript mapped types to ensure only valid WidgetType and shape combinations can be registered.

Extending the System: Adding a New Widget

To add a "Hat" widget, create the SVG file and extend the registry:

  1. Create src/assets/widgets/hat/fedora.svg containing $fillColor placeholders.
  2. Register the lazy import in dynamic-data.ts:
[WidgetType.Hat]: {
  [HatShape.Fedora]: () => import(`../assets/widgets/hat/fedora.svg?raw`),
},
  1. Include the widget in avatar options. The component automatically loads fedora.svg only when this configuration is active, maintaining the lazy-loading behavior.

Summary

  • The dynamic SVG loading mechanism centers on a registry pattern in src/utils/dynamic-data.ts that maps widget types to Vite dynamic imports using the ?raw suffix.
  • Runtime assembly occurs in src/components/VueColorAvatar.vue, where promises resolve to raw SVG strings that are processed, colored, and composited into a single avatar image.
  • The system leverages code-splitting to load only required assets, reducing initial bundle size while supporting unlimited widget expansions.
  • Color customization happens via string replacement of $fillColor placeholders before DOM insertion, enabling dynamic theming without multiple SVG variants.

Frequently Asked Questions

How does Vite's ?raw parameter affect the import?

Vite's ?raw query parameter instructs the bundler to import the file contents as a plain text string rather than as a parsed module or component. According to the vue-color-avatar source code, this allows the application to receive the SVG markup directly via .default, enabling string manipulation methods like replaceAll for color injection before inserting the content into the DOM.

What prevents all SVGs from loading at once?

The lazy-evaluation pattern used in widgetData ensures that import functions remain unexecuted until explicitly called by the component's watchEffect. Each registry entry is a function returning import(), not the import itself, so Vite can code-split each SVG into separate chunks that fetch only when the user selects specific avatar options.

How does the system handle widget layering order?

The component sorts widgets using zIndex values defined in AVATAR_LAYER constants before constructing the import promises. This guarantees that facial features render in the correct sequence (e.g., ears behind faces), regardless of the order in which the asynchronous imports resolve.

Is this dynamic loading approach type-safe?

Yes. The widgetData registry uses TypeScript's mapped types ([key in \${WidgetType}`]) to enforce that only valid widget types and shapes can be registered. This compile-time checking ensures that widgetData[widgetType][opt.shape]` access remains safe, preventing undefined function calls during the dynamic import resolution phase.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →