Skip to content

VIcon

🌟 Introduction

Le composant VIcon est un composant Vue.js permettant d'afficher des icônes avec une large gamme d'options de personnalisation, y compris des animations, des couleurs, et des tailles. Il est conçu pour être flexible et performant, avec une prise en charge des différentes options d'affichage, de flip, et de titres accessibles.

Il a exactement la même API que OhVueIcon, et utilise @iconify/vue sous le capot.

Attention

Les noms des icônes doivent être ceux de Iconify-vue.

📐 Structure

Le composant VIcon s'intègre facilement en utilisant la syntaxe suivante :

vue
<VIcon name="nom-collection:nom-de-l-icone" :scale="1.5" color="#FF5733" animation="spin" />
vue
<VIcon name="ri:alert-fill" :scale="1.5" color="#FF5733" animation="ring" />

Migration depuis 5.x

Pour les noms de collection qui ne contiennent pas de tiret (-), il est accepté de séparer le nom de la collection du nom de l’icône avec un tiret -.

vue
<VIcon name="ri-alert-fill" :scale="1.5" color="#FF5733" animation="ring" />

Ceci rend le composant VIcon totalement compatible avec OhVueIcon si n’étaient utilisées que les icônes RemixIcon et quelques autres collections.

DX

Pour l’expérience développeur, il est conseillé d’utiliser l’extension vscode antfu.iconify.

🛠️ Props

Voici les différentes propriétés que vous pouvez utiliser avec ce composant :

PropTypeDéfautDescription
namestringObligatoireLe nom de l'icône à afficher.
scalestring | number1Échelle de l'icône, avec un facteur multiplicateur de la taille par défaut.
verticalAlignstring'-0.2em'Alignement vertical de l'icône par rapport à la ligne de base.
animation'spin' | 'wrench' | 'pulse' | 'spin-pulse' | 'flash' | 'float'undefinedType d'animation appliqué à l'icône.
speed'fast' | 'slow'undefinedVitesse de l'animation si elle est définie.
flip'horizontal' | 'vertical' | 'both'undefinedInverse l'icône horizontalement, verticalement ou les deux.
labelstringundefinedÉtiquette ARIA pour l'accessibilité.
titlestringundefinedTitre de l'icône (balise <title>), utilisé pour l'accessibilité et les info-bulles.
colorstringundefinedCouleur principale de l'icône.
fillstringundefinedCouleur de remplissage de l'icône (utilise in fine color comme conseillé dans la doc de @iconify/vue). Cette prop n’existe que pour la rétrocompatibilité avec OhVueIcon, préférer l’utilisation de la prop color.
inversebooleanfalseApplique une couleur inversée à l'icône.
ssrbooleanfalseActive le rendu côté serveur (Server-Side Rendering).
display'block' | 'inline-block' | 'inline''inline-block'Définit le mode d'affichage de l'icône.

🔄 Optimisation SSR (🆕 Amélioré)

Le composant VIcon gère intelligemment les problèmes d'hydratation SSR avec une approche simplifiée :

  • ssr: false (défaut) : L'icône est rendue uniquement côté client, évitant tous les problèmes d'hydratation
  • ssr: true : L'icône est rendue côté serveur avec un fallback temporaire jusqu'à la fin du montage du composant
  • Un symbole temporaire (⏳) est affiché brièvement avant l'affichage de l'icône si ssr: true
vue
<!-- Recommandé pour la plupart des cas -->
<VIcon name="ri:home-line" />

<!-- Pour les icônes critiques nécessitant un SSR -->
<VIcon name="ri:menu-line" :ssr="true" />

Bonnes pratiques

  • Utilisez ssr: false (défaut) pour la plupart des icônes
  • Utilisez ssr: true seulement pour les icônes critiques (navigation, logo, etc.)
  • Le fallback temporaire disparaît automatiquement après le montage du composant
  • Aucun délai artificiel n'est utilisé, optimisant les performances

📡Événements

Ce composant ne déclenche pas d'événements personnalisés.

🧩 Slots

Ce composant ne contient pas de slots.

⚙️ Code source du composant

vue
<script lang="ts" setup>
import VIcon from '../VIcon.vue'
</script>

<template>
  <div class="flex  justify-between p-4">
    <section>
      <header class="fr-text--lg">
        Simple :
      </header>
      <p>
        <VIcon
          name="ri-close-line"
        />
        <VIcon
          name="ri-checkbox-circle-line"
        />
      </p>
    </section>
    <section>
      <header class="fr-text--lg">
        Plus grande :
      </header>
      <p>
        <VIcon
          name="ri-close-line"
          scale="2"
        />
        <VIcon
          name="ri-checkbox-circle-line"
          scale="2"
        />
      </p>
    </section>
    <section>
      <header class="fr-text--lg">
        Animée :
      </header>
      <p>
        spin :
        <VIcon
          name="ri-loader-4-line"
          animation="spin"
        />
      </p>
      <p>
        spin-pulse :
        <VIcon
          name="ri-loader-4-line"
          animation="spin-pulse"
        />
      </p>
      <p>
        pulse :
        <VIcon
          name="ri-checkbox-circle-line"
          animation="pulse"
        />
      </p>
      <p>
        flash :
        <VIcon
          name="ri-close-line"
          animation="flash"
        />
      </p>
      <p>
        float :
        <VIcon
          name="ri-close-line"
          animation="float"
        />
      </p>
      <p>
        ring :
        <VIcon
          name="fa6-regular:bell"
          animation="ring"
        />
      </p>
      <p>
        wrench :
        <VIcon
          name="twemoji:rolling-on-the-floor-laughing"
          animation="wrench"
        />
      </p>
    </section>
    <section>
      <header class="fr-text--lg">
        Colorée :
      </header>
      <p>
        lightgreen :
        <VIcon
          name="ri-checkbox-circle-line"
          color="lightgreen"
        />
      </p>
      <p>
        #f90 :
        <VIcon
          name="ri-checkbox-circle-line"
          color="#f90"
        />
      </p>
      <p>
        var(--blue-france-main-525) :
        <VIcon
          name="ri-checkbox-circle-line"
          color="var(--blue-france-main-525)"
        />
      </p>
    </section>
  </div>
</template>
vue
<script lang="ts" setup>
import type { VIconProps } from './VIcon.types'

import { Icon } from '@iconify/vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'

export type { VIconProps }

const props = withDefaults(defineProps<VIconProps>(), {
  scale: 1,
  verticalAlign: '-0.2em',
  display: 'inline-block',
  ssr: false, // Changement : ssr false par défaut pour éviter les problèmes d'hydratation
})

const icon = ref<{ $el: SVGElement } | null>(null)
const isMounted = ref(false)

const fontSize = computed(() => `${+props.scale * 1.2}rem`)
const flip = computed(() => {
  if (props.flip === 'both') {
    return 'horizontal,vertical'
  }
  return props.flip
})

watch(() => props.title, setTitle)

async function setTitle () {
  if (!(icon.value?.$el)) {
    return
  }
  const titleExists = !!(icon.value?.$el).querySelector('title')
  const titleEl = document.createElement('title')
  if (!props.title) {
    titleEl.remove()
    return
  }
  titleEl.innerHTML = props.title
  await nextTick()
  if (!titleExists) {
    (icon.value?.$el as SVGElement).firstChild?.before(titleEl)
  }
}

onMounted(() => {
  // Hydratation terminée, on peut maintenant afficher l'icône en toute sécurité
  isMounted.value = true
  setTitle()
})

const finalName = computed(() => {
  return props.name?.startsWith('vi-') ? props.name.replace(/vi-(.*)/, 'vscode-icons:$1') : props.name ?? ''
})
const finalColor = computed(() => {
  return props.color ?? props.fill ?? 'inherit'
})
</script>

<template>
  <!-- Rendu conditionnel simple :
       - Si ssr=false (défaut) : affiche directement l'icône
       - Si ssr=true : attend que le composant soit monté (hydratation terminée) -->
  <Icon
    v-if="!props.ssr || isMounted"
    ref="icon"
    :icon="finalName"
    :style="{ fontSize, verticalAlign, display, color: finalColor }"
    :aria-label="props.label"
    class="vicon"
    :class="{
      'vicon-spin': props.animation === 'spin',
      'vicon-wrench': props.animation === 'wrench',
      'vicon-pulse': props.animation === 'pulse',
      'vicon-spin-pulse': props.animation === 'spin-pulse',
      'vicon-flash': props.animation === 'flash',
      'vicon-float': props.animation === 'float',
      'vicon-ring': props.animation === 'ring',
      'vicon-slow': props.speed === 'slow',
      'vicon-fast': props.speed === 'fast',
      'vicon-inverse': props.inverse,
    }"
    :flip
    :ssr="props.ssr && isMounted"
  />
  <!-- Placeholder pendant l'attente du montage (seulement si ssr=true) -->
  <span
    v-else-if="props.ssr"
    :style="{ fontSize, verticalAlign, display, color: finalColor, opacity: 0.7 }"
    :aria-label="props.label"
    class="vicon vicon-loading"
    role="img"
  >

  </span>
</template>

<style scoped>
.vicon-inverse {
  color: #fff !important;
}

.vicon-loading {
  /* Styles pour le fallback pendant l'hydratation */
  transition: opacity 0.2s ease;
  user-select: none;
}

/* ---------------- spin ---------------- */
.vicon-spin:not(.vicon-hover),
.vicon-spin.vicon-hover:hover,
.vicon-parent.vicon-hover:hover > .vicon-spin {
  animation: vicon-spin 1s linear infinite;
}

.vicon-spin:not(.vicon-hover).vicon-fast,
.vicon-spin.vicon-hover.vicon-fast:hover,
.vicon-parent.vicon-hover:hover > .vicon-spin.vicon-fast {
  animation: vicon-spin 0.7s linear infinite;
}

.vicon-spin:not(.vicon-hover).vicon-slow,
.vicon-spin.vicon-hover.vicon-slow:hover,
.vicon-parent.vicon-hover:hover > .vicon-spin.vicon-slow {
  animation: vicon-spin 2s linear infinite;
}

/* ---------------- spin-pulse ---------------- */

.vicon-spin-pulse:not(.vicon-hover),
.vicon-spin-pulse.vicon-hover:hover,
.vicon-parent.vicon-hover:hover > .vicon-spin-pulse {
  animation: vicon-spin 1s infinite steps(8);
}

.vicon-spin-pulse:not(.vicon-hover).vicon-fast,
.vicon-spin-pulse.vicon-hover.vicon-fast:hover,
.vicon-parent.vicon-hover:hover > .vicon-spin-pulse.vicon-fast {
  animation: vicon-spin 0.7s infinite steps(8);
}

.vicon-spin-pulse:not(.vicon-hover).vicon-slow,
.vicon-spin-pulse.vicon-hover.vicon-slow:hover,
.vicon-parent.vicon-hover:hover > .vicon-spin-pulse.vicon-slow {
  animation: vicon-spin 2s infinite steps(8);
}

@keyframes vicon-spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/* ---------------- wrench ---------------- */
.vicon-wrench:not(.vicon-hover),
.vicon-wrench.vicon-hover:hover,
.vicon-parent.vicon-hover:hover > .vicon-wrench {
  animation: vicon-wrench 2.5s ease infinite;
}

.vicon-wrench:not(.vicon-hover).vicon-fast,
.vicon-wrench.vicon-hover.vicon-fast:hover,
.vicon-parent.vicon-hover:hover > .vicon-wrench.vicon-fast {
  animation: vicon-wrench 1.2s ease infinite;
}

.vicon-wrench:not(.vicon-hover).vicon-slow,
.vicon-wrench.vicon-hover.vicon-slow:hover,
.vicon-parent.vicon-hover:hover > .vicon-wrench.vicon-slow {
  animation: vicon-wrench 3.7s ease infinite;
}

@keyframes vicon-wrench {
  0% {
    transform: rotate(-12deg);
  }

  8% {
    transform: rotate(12deg);
  }

  10%, 28%, 30%, 48%, 50%, 68% {
    transform: rotate(24deg);
  }

  18%, 20%, 38%, 40%, 58%, 60% {
    transform: rotate(-24deg);
  }

  75%, 100% {
    transform: rotate(0deg);
  }
}

/* ---------------- ring ---------------- */
.vicon-ring:not(.vicon-hover),
.vicon-ring.vicon-hover:hover,
.vicon-parent.vicon-hover:hover > .vicon-ring {
  animation: vicon-ring 2s ease infinite;
}

.vicon-ring:not(.vicon-hover).vicon-fast,
.vicon-ring.vicon-hover.vicon-fast:hover,
.vicon-parent.vicon-hover:hover > .vicon-ring.vicon-fast {
  animation: vicon-ring 1s ease infinite;
}

.vicon-ring:not(.vicon-hover).vicon-slow,
.vicon-ring.vicon-hover.vicon-slow:hover,
.vicon-parent.vicon-hover:hover > .vicon-ring.vicon-slow {
  animation: vicon-ring 3s ease infinite;
}

@keyframes vicon-ring {
  0% {
    transform: rotate(-15deg);
  }

  2% {
    transform: rotate(15deg);
  }

  4%, 12% {
    transform: rotate(-18deg);
  }

  6% {
    transform: rotate(18deg);
  }

  8% {
    transform: rotate(-22deg);
  }

  10% {
    transform: rotate(22deg);
  }

  12% {
    transform: rotate(-18deg);
  }

  14% {
    transform: rotate(18deg);
  }

  16% {
    transform: rotate(-12deg);
  }

  18% {
    transform: rotate(12deg);
  }

  20%, 100% {
    transform: rotate(0deg);
  }
}

/* ---------------- pulse ---------------- */
.vicon-pulse:not(.vicon-hover),
.vicon-pulse.vicon-hover:hover,
.vicon-parent.vicon-hover:hover > .vicon-pulse {
  animation: vicon-pulse 2s linear infinite;
}

.vicon-pulse:not(.vicon-hover).vicon-fast,
.vicon-pulse.vicon-hover.vicon-fast:hover,
.vicon-parent.vicon-hover:hover > .vicon-pulse.vicon-fast {
  animation: vicon-pulse 1s linear infinite;
}

.vicon-pulse:not(.vicon-hover).vicon-slow,
.vicon-pulse.vicon-hover.vicon-slow:hover,
.vicon-parent.vicon-hover:hover > .vicon-pulse.vicon-slow {
  animation: vicon-pulse 3s linear infinite;
}

@keyframes vicon-pulse {
  0% {
    transform: scale(1.1);
  }

  50% {
    transform: scale(0.8);
  }

  100% {
    transform: scale(1.1);
  }
}

/* ---------------- flash ---------------- */
.vicon-flash:not(.vicon-hover),
.vicon-flash.vicon-hover:hover,
.vicon-parent.vicon-hover:hover > .vicon-flash {
  animation: vicon-flash 2s ease infinite;
}

.vicon-flash:not(.vicon-hover).vicon-fast,
.vicon-flash.vicon-hover.vicon-fast:hover,
.vicon-parent.vicon-hover:hover > .vicon-flash.vicon-fast {
  animation: vicon-flash 1s ease infinite;
}

.vicon-flash:not(.vicon-hover).vicon-slow,
.vicon-flash.vicon-hover.vicon-slow:hover,
.vicon-parent.vicon-hover:hover > .vicon-flash.vicon-slow {
  animation: vicon-flash 3s ease infinite;
}

@keyframes vicon-flash {
  0%, 100%, 50%{
    opacity: 1;
  }
  25%, 75%{
    opacity: 0;
  }
}

/* ---------------- float ---------------- */
.vicon-float:not(.vicon-hover),
.vicon-float.vicon-hover:hover,
.vicon-parent.vicon-hover:hover > .vicon-float {
  animation: vicon-float 2s linear infinite;
}

.vicon-float:not(.vicon-hover).vicon-fast,
.vicon-float.vicon-hover.vicon-fast:hover,
.vicon-parent.vicon-hover:hover > .vicon-float.vicon-fast {
  animation: vicon-float 1s linear infinite;
}

.vicon-float:not(.vicon-hover).vicon-slow,
.vicon-float.vicon-hover.vicon-slow:hover,
.vicon-parent.vicon-hover:hover > .vicon-float.vicon-slow {
  animation: vicon-float 3s linear infinite;
}

@keyframes vicon-float {
  0%, 100% {
    transform: translateY(-3px);
  }
  50% {
    transform: translateY(3px);
  }
}
</style>