Skip to content

Accordéon - DsfrAccordion

🌟 Introduction

Les accordéons permettent aux utilisateurs d'afficher et de masquer des sections de contenu présentés dans une page.

🏅 La documentation sur l’accordéon sur le DSFR

La story sur l’accordéon sur le storybook de VueDsfr

📐 Structure

Un accordéon est constitué des éléments suivants :

  • un en-tête (prop title, de type string), correspondant au titre de la section - obligatoire.
  • une icône, indique quand le panneau est fermé, quand le panneau est ouvert.
  • un séparateur
  • une zone de contenu, masqué par défaut pouvant contenir tout type d'élément, le slot par défaut est fait pour ça

Autres props :

  • id : identifiant du contenu de l’accordéon, qui est utilisé aussi pour l’attribut aria-controls du bouton qui permet de plier et déplier l’accordéon
  • expandedId : identifiant de l’accordéon actuellement déplié (pour savoir si l’accordéon doit être déplié)

🛠️ Props

NomTypeDéfautObligatoire
titlestring
titleTagTitleTag'h3'
idstringrandom string
expandedIdstringundefined

📡 Évenements

DsfrAccordion déclenche l’événement 'expand' au clic sur le titre de l’accordéon.

nomdonnée (payload)détail de la donnée
'expand'stringIl s’agit de l’id de l’accordéon (sa prop id)

Ceci permet de récupérer l’id et de passer la prop expandedId à tous les accordéons du groupe (voir le code de la démo ci-dessous).

🧩 Slots

DsfrAccordion possède un slot par défaut pour le contenu de l’accordéon.

🏗️ Les groupes d’accordéons - DsfrAccordionGroup

Un accordéon prend plus de sens lorsqu’il fait partie d’un groupe (comme nous tous, non ?).

Les accordéons sont tous fermés et comprennent l’en tête et l’icône si la prop expandedId est undefined.

La totalité de la barre de titre est cliquable. L’événement clic sur le titre de l’accordéon peut être intercepté en ajoutant un écouteur sur l’événement expand qui envoie l’id de l’accordéon :

vue
<script lang="ts" setup>
// (...)
const title1 = ref('Un titre d’accordéon 1')
const title2 = ref('Un titre d’accordéon 2')
// (...)
const expandedId = ref('')
</script>

<template>
  <DsfrAccordionsGroup>
    <li>
      <DsfrAccordion
        id="accordion-1"
        :title="title1"
        :expanded-id="expandedId"
        @expand="expandedId = $event"
      >
        Contenu de l’accordéon 1
      </DsfrAccordion>
    </li>
    <li>
      <DsfrAccordion
        id="accordion-2"
        :title="title2"
        :expanded-id="expandedId"
        @expand="id => expandedId = id"
      >
        Contenu de l’accordéon 2
      </DsfrAccordion>
    </li>
    <!-- (...) -->
</template>

TIP

Comme l’événement expand a comme donnée (payload) l’id de l’accordéon, les deux lignes suivantes sont équivalentes :

vue
@expand="id => expandedId = id"

et

vue
@expand="expandedId = $event"

Cf. la documentation Vue pour plus de détails.

📝 Exemple complet

vue
<script lang="ts" setup>
import { ref } from 'vue'

import DsfrAccordion from '../DsfrAccordion.vue'
import DsfrAccordionsGroup from '../DsfrAccordionsGroup.vue'

const title1 = ref('Un titre d’accordéon 1')
const title2 = ref('Un titre d’accordéon 2')
const title3 = ref('Un titre d’accordéon 3')
const expandedId = ref<string>()
</script>

<template>
  <DsfrAccordionsGroup>
    <li>
      <DsfrAccordion
        id="accordion-1"
        :title="title1"
        :expanded-id="expandedId"
        @expand="expandedId = $event"
      >
        Contenu de l’accordéon 1
      </DsfrAccordion>
    </li>
    <li>
      <DsfrAccordion
        id="accordion-2"
        :title="title2"
        :expanded-id="expandedId"
        @expand="id => expandedId = id"
      >
        Contenu de l’accordéon 2
      </DsfrAccordion>
    </li>
    <li>
      <DsfrAccordion
        id="accordion-3"
        :title="title3"
        :expanded-id="expandedId"
        @expand="id => expandedId = id"
      >
        Contenu de l’accordéon 3
      </DsfrAccordion>
    </li>
  </DsfrAccordionsGroup>
</template>
vue
<script lang="ts" setup>
import { computed, onMounted, watch } from 'vue'

import { getRandomId } from '../../utils/random-utils'
import { useCollapsable } from '../../composables'
import type { DsfrAccordionProps } from './DsfrAccordion.types'

export type { DsfrAccordionProps }

const props = withDefaults(
  defineProps<DsfrAccordionProps>(),
  {
    id: () => getRandomId('accordion'),
    expandedId: undefined,
    title: 'Sans intitulé',
    titleTag: 'h3',
  },
)

const emit = defineEmits<{ (event: 'expand', id: string | undefined): void }>()

const {
  collapse,
  collapsing,
  cssExpanded,
  doExpand,
  onTransitionEnd,
} = useCollapsable()

const expanded = computed(() => props.expandedId === props.id)

onMounted(() => {
  // Accordion can be expanded by default
  // We need to trigger the expand animation on mounted
  if (expanded.value) {
    doExpand(true)
  }
})

watch(expanded, (newValue, oldValue) => {
  /*
  * @see https://github.com/GouvernementFR/dsfr/blob/main/src/core/script/collapse/collapse.js
  */
  if (newValue !== oldValue) {
    doExpand(newValue)
  }
})

const toggleExpanded = () => {
  /*
   * Close current accordion if expanded
   */
  emit('expand', expanded.value ? undefined : props.id)
}
</script>

<template>
  <section class="fr-accordion">
    <component
      :is="titleTag"
      class="fr-accordion__title"
    >
      <button
        class="fr-accordion__btn"
        :aria-expanded="expanded"
        :aria-controls="id"
        type="button"
        @click="toggleExpanded()"
      >
        <!-- @slot Slot pour le contenu personnalisé du titre de l’accordéon. Une **props du même nom est utilisable pour du texte simple** sans mise en forme. -->
        <slot name="title">
          <span>{{ title }}</span>
        </slot>
      </button>
    </component>
    <div
      :id="id"
      ref="collapse"
      class="fr-collapse"
      :class="{
        'fr-collapse--expanded': cssExpanded, // Need to use a separate data to add/remove the class after a RAF
        'fr-collapsing': collapsing,
      }"
      @transitionend="onTransitionEnd(expanded)"
    >
      <!-- @slot Slot par défaut pour le contenu de l’accordéon: sera dans `<div class="fr-collapse">` -->
      <slot />
    </div>
  </section>
</template>
vue
<script setup lang="ts">
import { ref } from 'vue'

const expandedId = ref<string>()

const expand = (id: string): void => { expandedId.value = id }
</script>

<template>
  <ul
    class="fr-accordions-group"
    @expand="expand($event)"
  >
    <!-- @slot Slot par défaut pour le contenu de la liste. Sera dans `<ul class="fr-accordions-group">` -->
    <slot :expanded-id="expandedId" />
  </ul>
</template>