SVG Icons in Vue 3: The Complete Guide (lucide-vue-next, Iconify & unplugin-icons)

How to use SVG icons in Vue 3 and Nuxt 3 — lucide-vue-next, @iconify/vue, and unplugin-icons with auto-import. Code patterns, tree-shaking, and dark mode.

Amit Yadav
Amit Yadav

Vue 3’s Composition API and Nuxt 3’s auto-import system make SVG icon integration cleaner than in any previous Vue version. Three tools dominate the ecosystem: lucide-vue-next for a Lucide-equivalent experience, @iconify/vue for access to 250,000+ icons, and unplugin-icons for automatic on-demand icon imports.

This guide covers all three with Vue 3 and Nuxt 3 patterns.

Option 1: lucide-vue-next

lucide-vue-next is the official Lucide port for Vue 3. Same 1,500+ icons, same MIT license, same prop API — but exported as Vue SFCs.

npm install lucide-vue-next

Usage in Vue 3

<script setup>
import { Search, Settings, Bell } from 'lucide-vue-next';
</script>

<template>
  <div class="flex gap-4 text-gray-700 dark:text-gray-300">
    <Search :size="20" />
    <Settings :size="20" />
    <Bell :size="20" :stroke-width="1.5" />
  </div>
</template>
Props use kebab-case in templates

Vue templates use kebab-case for props: :stroke-width="1.5" not strokeWidth. The <script setup> import is the same as React.

Props reference

PropTypeDefaultNotes
sizenumber24Width and height
colorstringcurrentColorSVG stroke color
stroke-widthnumber2Stroke width
classstringCSS class

Global registration in Nuxt 3

For icons used app-wide, register them as global components in a plugin:

// plugins/icons.ts
import { defineNuxtPlugin } from '#app';
import { Search, Bell, Settings, User } from 'lucide-vue-next';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('IconSearch', Search);
  nuxtApp.vueApp.component('IconBell', Bell);
  nuxtApp.vueApp.component('IconSettings', Settings);
  nuxtApp.vueApp.component('IconUser', User);
});

Then use without importing in every component:

<template>
  <IconSearch :size="18" />
</template>

Option 2: @iconify/vue

@iconify/vue gives you access to every icon in the Iconify ecosystem — Lucide, Tabler, Phosphor, Heroicons, Material Symbols, and 250,000+ more — through a single component.

npm install @iconify/vue

Basic usage

<script setup>
import { Icon } from '@iconify/vue';
</script>

<template>
  <!-- Format: pack:icon-name -->
  <Icon icon="lucide:search" class="w-5 h-5" />
  <Icon icon="tabler:dashboard" class="w-5 h-5" />
  <Icon icon="ph:heart-duotone" class="w-5 h-5 text-blue-500" />
</template>

Offline / SSR mode with bundled icons

By default, @iconify/vue fetches icons from the Iconify CDN at runtime. For SSR (Nuxt) or offline builds, bundle the icons you use:

npm install @iconify/json
# or install individual packs:
npm install @iconify-icons/lucide
<script setup>
import { Icon } from '@iconify/vue';
import searchIcon from '@iconify-icons/lucide/search';
</script>

<template>
  <Icon :icon="searchIcon" class="w-5 h-5" />
</template>
Use @iconify/vue for prototyping, lucide-vue-next for production

@iconify/vue’s runtime API is fast and great for exploration. For a production app with a known icon set, switch to bundled icons or lucide-vue-next for zero runtime fetch overhead.

unplugin-icons is a Vite/Webpack plugin that auto-imports icons on demand — you reference them in templates, the plugin resolves and bundles only the icons you use, with no explicit import needed.

npm install -D unplugin-icons
npm install @iconify/json  # icon data source

Vite setup (Vue 3)

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Icons from 'unplugin-icons/vite';
import Components from 'unplugin-vue-components/vite';
import { IconsResolver } from 'unplugin-icons/resolver';

export default defineConfig({
  plugins: [
    vue(),
    Icons({ compiler: 'vue3' }),
    Components({
      resolvers: [IconsResolver()],
    }),
  ],
});

Nuxt 3 setup

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['unplugin-icons/nuxt'],
  icons: {
    // global config
  },
});

Usage (auto-resolved, no imports)

With the resolver configured, components are named I{Pack}{IconName}:

<template>
  <!-- Lucide: I + Lucide + IconName (PascalCase) -->
  <ILucideSearch class="w-5 h-5" />
  <ILucideSettings class="w-5 h-5" />

  <!-- Tabler: I + Tabler + IconName -->
  <ITablerDashboard class="w-5 h-5" />

  <!-- Phosphor: I + Ph + IconName -->
  <IPhHeart class="w-5 h-5 text-red-500" />
</template>

No import statement required. The plugin resolves and bundles only Search, Settings, Dashboard, and Heart — zero unused icons in the final bundle.

TypeScript autocomplete for unplugin-icons

Add unplugin-icons/types/vue to your tsconfig.json includes for TypeScript to recognize the auto-generated component names.

Dark mode patterns in Vue

All three approaches use currentColor by default, so Tailwind’s dark mode works without extra configuration:

<template>
  <!-- Tailwind dark: variant — icon inherits text color -->
  <div class="text-gray-700 dark:text-gray-300">
    <Search :size="20" />
  </div>
</template>

For CSS variable-based theming:

<style>
:root { --icon-default: theme(colors.gray.700); }
.dark { --icon-default: theme(colors.gray.300); }
</style>

<template>
  <Search :size="20" style="color: var(--icon-default)" />
</template>

Accessibility in Vue templates

<template>
  <!-- Decorative icon in labeled button -->
  <button>
    <Download :size="18" aria-hidden="true" />
    Export CSV
  </button>

  <!-- Standalone meaningful icon -->
  <Search
    :size="20"
    role="img"
    aria-label="Search"
  />

  <!-- Icon-only button -->
  <button aria-label="Delete item">
    <Trash2 :size="18" aria-hidden="true" />
  </button>
</template>

Choosing between the three approaches

NeedRecommended
Lucide icons only, max tree-shakinglucide-vue-next
Multiple icon packs in one project@iconify/vue
Zero-import convenience in Nuxt/Viteunplugin-icons
SSR-safe with bundled icons@iconify/vue with offline mode
Smallest possible bundlelucide-vue-next (named imports)
Share this post