Skip to content

Concept

Icons in user interfaces come with several challenges:

  • Accessibility: Follow semantic best practices for screen readers - see Accessible SVG Icons
  • Consistency: style, size, and color to create a cohesive user experience.
  • Reusability: reusable across different components and pages to reduce redundancy.
  • Performance: Avoid too long loading times and large bundle sizes.
  • Customization: Adapt to different themes and brandings

A typical approach is to create an icon component library — a collection of reusable UI components that can be used by developers to build user interfaces:

  • Auto-imported like any other component
  • Typed for IDE autocompletion and safety
  • Styled consistently via classes or CSS variables
  • Reusable across different components and pages

Motivation

Icon components should be easy to use, style, and maintain.

Existing solutions often force trade-offs between DX, accessibility, and flexibility. In a Vue or Nuxt codebase, we often lead to different solutions:

1. Third-party icon libraries

Many libraries will wrap your existing icons or directly provide an existing list of icons, and standardize the API

vue
<template>
  <!-- either access the icon using a "name" prop  -->
  <icon name="home" />
  <!-- or naming the icon from a list of available icons -->
  <icon-home />
  <IconHome />
</template>

It's fast and efficient, but can present some limitations in terms of customizations, and often result in a wrapper div or span tag around the SVG icon in the HTML template, which can be a problem for styling and accessibility.

Example output:

html
<div class="icon-wrapper">
  <!-- The real icon -->
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
  >
    <!-- ... -->
  </svg>
</div>
ProsCons
+ Easy to integrateExtra wrapper elements (<div>, <span>)
+ Predefined icon setsLimited styling flexibility
+ Standardized APIIcons may not match your brand or design system
Styling often relies on props or global rules

2. Manual .vue components

Each SVG is manually wrapped into a Vue component.

vue
<template>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    :stroke="currentColor"
    :stroke-width="strokeWidth"
    stroke-linecap="round"
    stroke-linejoin="round"
  >
    <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
    <path d="M2 17l10 5 10-5M12 2v15"></path>
  </svg>
</template>
<!-- Or a wrapper <BaseIcon>...</BaseIcon> -->

This solution is more flexible, as it gives more control over each SVG, but can be time-consuming and repetitive, especially when you have a lot of icons to manage.

ProsCons
- Full control over markup and attributes- Repetitive and hard to scale
- No wrappers- No type safety
- Easy to align with a design system- No auto-imports or generation tooling
- Easy to customize- No auto-imports or generation tooling

3. SVG imports via loaders

Tools like vite-svg-loader or vue-svg-loader allow importing SVGs directly as components:

vue
<template>
  <Icon />
</template>

<script setup>
import Icon from '@/icons/user-badge.svg';
</script>
ProsCons
Clean output with no wrappers- Requires manual imports
Direct control over SVGs- No consistent naming or typing
Easy to organize icon files- No auto-registration or integration with Nuxt components
- No built-in support for theming or runtime styling

Nuxt Compose Icons

The goal of this module is to propose a balanced approach which gives design flexibility and developer experience.

It dynamically generates Vue components from initial SVG files, naming them accordingly and make them accessible as individual components in the Nuxt project.