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

> Explore the dynamic SVG loading mechanism in Vue-Color-Avatar using JavaScript import and Vite raw query for on demand loading and runtime color customization.

- Repository: [LeoKu/vue-color-avatar](https://github.com/codennnn/vue-color-avatar)
- Tags: deep-dive
- Published: 2026-02-27

---

**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`](https://github.com/codennnn/vue-color-avatar/blob/main/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.

```typescript
// 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`](https://github.com/codennnn/vue-color-avatar/blob/main/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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/codennnn/vue-color-avatar/blob/main/dynamic-data.ts):

```typescript
[WidgetType.Hat]: {
  [HatShape.Fedora]: () => import(`../assets/widgets/hat/fedora.svg?raw`),
},

```

3. 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`](https://github.com/codennnn/vue-color-avatar/blob/main/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`](https://github.com/codennnn/vue-color-avatar/blob/main/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.