Ensemble de Tags - DsfrTags 
🌟 Introduction 
Le composant DsfrTags permet d'afficher un groupe de tags interactifs et personnalisables. Il est particulièrement utile pour gérer des listes de filtres ou des catégories sélectionnables. Il s'appuie sur le composant DsfrTag et offre la possibilité de suivre l'état des tags sélectionnés via une liaison avec v-model.
🏅 La documentation sur le tag sur le DSFR
La story sur le tag sur le storybook de VueDsfr📐 Structure 
Ce composant affiche une liste de tags sous forme de <ul> et permet d'associer un modèle réactif (v-model) pour suivre les sélections des tags interactifs.
🛠️ Props 
| Nom | Type | Par défaut | Description | 
|---|---|---|---|
| tags | DsfrTagProps<T>[] | [] | Liste des tags à afficher. | 
| modelValue | T[] | undefined | Liste des valeurs des tags sélectionnés (si les tags sont sélectionnables). | 
📡 Événements 
| Nom | Paramètres | Description | 
|---|---|---|
| update:modelValue | T[] | Émis lorsqu'un tag sélectionnable est (dé)sélectionné, mettant à jour la liste des valeurs sélectionnées. | 
🧩 Slots 
(Aucun slot spécifique, chaque tag étant généré automatiquement en fonction de la liste fournie en props.)
📝 Exemples 
vue
<script lang="ts" setup>
import type { DsfrTagProps } from '@/components/DsfrTag/DsfrTags.types.ts'
import { computed, ref } from 'vue'
import DsfrTags from '@/components/DsfrTag/DsfrTags.vue'
const tagSet: (DsfrTagProps)[] = [
  {
    label: 'Les fruits',
    selectable: true,
    selected: true,
    value: 'fruit',
  },
  {
    label: 'Les légumes',
    selectable: true,
    value: 'legume',
  },
]
type FruitOrVegetable = 'fruit' | 'legume'
const items: { name: string, type: FruitOrVegetable }[] = [
  {
    name: 'Banane',
    type: 'fruit',
  },
  {
    name: 'Pomme',
    type: 'fruit',
  },
  {
    name: 'Poire',
    type: 'fruit',
  },
  {
    name: 'Courgette',
    type: 'legume',
  },
  {
    name: 'Poivron',
    type: 'legume',
  },
  {
    name: 'Navet',
    type: 'legume',
  },
]
const filters = ref<FruitOrVegetable[]>(['fruit', 'legume'])
const filteredItems = computed(() =>
  items // Get all items
    .filter(item => filters.value.includes(item.type)) // Filter according to filters
    .sort((a, b) => a.name > b.name ? 1 : -1), // Sort alphabetically
)
</script>
<template>
  <div class="max-w-90">
    <div class="fr-mt-2w">
      <DsfrTags
        v-model="filters"
        :tags="tagSet"
      />
    </div>
    <ul>
      <li
        v-for="item of filteredItems"
        :key="item.name"
      >
        {{ item.name }}
      </li>
    </ul>
  </div>
</template>
<style scoped>
.max-w-90 {
  max-width: 90%;
}
</style>⚙️ Code source du composant 
vue
<script lang="ts" setup generic="T = string">
import type { DsfrTagProps } from './DsfrTags.types'
import { computed } from 'vue'
import VIcon from '../VIcon/VIcon.vue'
const props = withDefaults(defineProps<DsfrTagProps<T>>(), {
  label: undefined,
  link: undefined,
  tagName: 'p',
  icon: undefined,
  disabled: undefined,
})
defineEmits<{
  select: [[unknown, boolean]]
}>()
const isExternalLink = computed(() => typeof props.link === 'string' && props.link.startsWith('http'))
const is = computed(() => {
  return props.link
    ? (isExternalLink.value ? 'a' : 'RouterLink')
    : (((props.disabled && props.tagName === 'p') || props.selectable) ? 'button' : props.tagName)
})
const linkProps = computed(() => {
  return { [isExternalLink.value ? 'href' : 'to']: props.link }
})
const dsfrIcon = computed(() => typeof props.icon === 'string' && props.icon.startsWith('fr-icon-'))
const defaultScale = computed(() => props.small ? 0.65 : 0.9)
const iconProps = computed(() => typeof props.icon === 'string'
  ? { scale: defaultScale.value, name: props.icon }
  : { scale: defaultScale.value, ...props.icon },
)
</script>
<template>
  <component
    :is="is"
    class="fr-tag"
    :disabled="disabled"
    :class="{
      'fr-tag--sm': small,
      [icon as string]: dsfrIcon,
      'fr-tag--icon-left': dsfrIcon,
    }"
    :aria-pressed="selectable ? selected : undefined"
    v-bind="{ ...linkProps, ...$attrs }"
    @click="!disabled && $emit('select', [value, selected])"
  >
    <VIcon
      v-if="props.icon && !dsfrIcon"
      :label="iconOnly ? label : undefined"
      :class="{ 'fr-mr-1v': !iconOnly }"
      v-bind="iconProps"
    />
    <template v-if="!iconOnly">
      {{ label }}
    </template>
    <!-- @slot Slot par défaut pour le contenu du tag -->
    <slot />
  </component>
</template>
<style scoped>
.ov-icon {
  margin-top: 0.1rem;
}
.fr-tag {
  align-items: center;
}
.success {
  color: var(--success);
  background-color: var(--bg-success);
}
.error {
  color: var(--error);
  background-color: var(--bg-error);
}
.warning {
  color: var(--warning);
  background-color: var(--bg-warning);
}
.info {
  color: var(--info);
  background-color: var(--bg-info);
}
</style>ts
import type VIcon from '../VIcon/VIcon.vue'
export type DsfrTagProps<T = string> = {
  label?: string
  link?: string
  tagName?: string
  icon?: string | InstanceType<typeof VIcon>['$props']
  disabled?: boolean
  small?: boolean
  iconOnly?: boolean
} & ({
  selectable: true
  selected?: boolean
  value?: T
} | {
  selectable?: false
})
export type DsfrTagsProps<T = string> = {
  tags: DsfrTagProps<T>[]
  modelValue?: T[]
}