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 :
<VIcon name="nom-collection:nom-de-l-icone" :scale="1.5" color="#FF5733" animation="spin" /><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 -.
<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 :
| Prop | Type | Défaut | Description |
|---|---|---|---|
name | string | Obligatoire | Le nom de l'icône à afficher. |
scale | string | number | 1 | Échelle de l'icône, avec un facteur multiplicateur de la taille par défaut. |
verticalAlign | string | '-0.2em' | Alignement vertical de l'icône par rapport à la ligne de base. |
animation | 'spin' | 'wrench' | 'pulse' | 'spin-pulse' | 'flash' | 'float' | undefined | Type d'animation appliqué à l'icône. |
speed | 'fast' | 'slow' | undefined | Vitesse de l'animation si elle est définie. |
flip | 'horizontal' | 'vertical' | 'both' | undefined | Inverse l'icône horizontalement, verticalement ou les deux. |
label | string | undefined | Étiquette ARIA pour l'accessibilité. |
title | string | undefined | Titre de l'icône (balise <title>), utilisé pour l'accessibilité et les info-bulles. |
color | string | undefined | Couleur principale de l'icône. |
fill | string | undefined | Couleur 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. |
inverse | boolean | false | Applique une couleur inversée à l'icône. |
ssr | boolean | false | Active 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'hydratationssr: 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
<!-- 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: trueseulement 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
<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><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>