Skip to content

Menu latéral

Le menu latéral est un système de navigation secondaire présentant une liste verticale de liens placée à côté du contenu.

Le composant DsfrSideMenu fournit une navigation latérale avec support du collapse/expand, gestion des éléments de menu imbriqués, et intégration avec le routeur Vue.

🌟 Introduction

Le menu latéral est un système de navigation secondaire présentant une liste verticale de liens placée à côté du contenu.

🏅 La documentation sur le menu latéral sur le DSFR

La story sur le menu latéral sur le storybook de VueDsfr

📐 Structure

Le menu latéral crée :

  • Un élément <nav> avec la classe fr-sidemenu
  • Un bouton de toggle pour réduire/expandre le menu
  • Un conteneur collapsible avec les éléments de menu
  • Support des liens externes et internes avec le routeur Vue
  • Gestion automatique des états actifs et expandés

🛠️ Props

nomtypedéfautobligatoiredescription
buttonLabelstring'Dans cette rubrique'Label associé au bouton en état responsive dont le rôle est de déplier le side menu.
idstring() => useRandomId(...)(optionnel) Valeur de l’attribut id du side menu. Par défaut, un id pseudo-aléatoire sera donné.
sideMenuListIdstring() => useRandomId(...)Identifiant de la liste de menu
collapseValuestring'-492px'Valeur de collapse CSS
menuItemsDsfrSideMenuListItemProps[]undefinedTableau d’objets contenant les props attendus par DsfrSideMenuList.
headingTitlestring''Titre de la rubrique (c’est le titre du menu latéral).
titleTagTitleTag'h3'Balise HTML pour le titre
focusOnExpandingbooleantrueFocus automatique lors de l'expansion

📡 Événements

DsfrSideMenu déclenche l'événement suivant :

nomdonnée (payload)description
toggleExpandstringÉmis lors du toggle d'expansion d'un élément

🧩 Slots

nomdescription
defaultContenu du menu latéral (remplace la liste par défaut)

📝 Exemples

Exemple d'utilisation basique du menu latéral

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

const menuItems = ref([
  { text: 'Accueil', to: '/' },
  { text: 'À propos', to: '/about' },
  {
    text: 'Services',
    menuItems: [
      { text: 'Service 1', to: '/service1' },
      { text: 'Service 2', to: '/service2' },
    ],
  },
])

const onToggleExpand = (id: string) => {
  console.log('Toggle expand:', id)
}
</script>

<template>
  <DsfrSideMenu
    heading-title="Navigation"
    :menu-items="menuItems"
    @toggle-expand="onToggleExpand"
  />
</template>

Exemple plus complet

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

import DsfrSideMenu from '../DsfrSideMenu.vue'

const headingTitle = 'Titre de la rubrique'
const menuItems = ref([
  {
    id: '11',
    to: '/rubrique-1',
    text: 'Premier titre de niveau 1',
  },
  {
    id: '12',
    text: 'Deuxième titre de niveau 1',
    active: true,
    menuItems: [
      {
        id: '21',
        to: '/rubrique-2/sous-rubrique-1',
        text: 'Premier titre de niveau 2',
      },
      {
        id: '22',
        text: 'Deuxième titre de niveau 2',
        active: true,
        menuItems: [
          {
            id: '31',
            to: '/rubrique-2/sous-rubrique-2/sous-sous-rubrique-1',
            text: 'Premier titre de niveau 3',
          },
          {
            id: '32',
            to: '/rubrique-2/sous-rubrique-2/sous-sous-rubrique-2',
            text: 'Deuxième titre de niveau 3',
            active: true,
          },
        ],
      },
    ],
  },
])
</script>

<template>
  <div class="fr-container fr-my-2w">
    <h2>SideMenu</h2>

    <DsfrSideMenu
      :heading-title="headingTitle"
      :menu-items="menuItems"
    />
  </div>
</template>

⚙️ Code source du composant

vue
<script lang="ts" setup>
import type { DsfrSideMenuProps } from './DsfrSideMenu.types'

import { ref, watch } from 'vue'

import { useCollapsable } from '../../composables'
import { useRandomId } from '../../utils/random-utils'

import DsfrSideMenuList from './DsfrSideMenuList.vue'

export type { DsfrSideMenuProps }

withDefaults(defineProps<DsfrSideMenuProps>(), {
  buttonLabel: 'Dans cette rubrique',
  id: () => useRandomId('sidemenu'),
  sideMenuListId: () => useRandomId('sidemenu', 'list'),
  collapseValue: '-492px',
  // @ts-expect-error this is really undefined
  menuItems: () => undefined,
  headingTitle: '',
  titleTag: 'h3',
  focusOnExpanding: true,
})

defineEmits<{ (e: 'toggleExpand', payload: string): void }>()

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

const expanded = ref(false)

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

<template>
  <nav
    class="fr-sidemenu"
    :aria-labelledby="id"
  >
    <div class="fr-sidemenu__inner">
      <button
        class="fr-sidemenu__btn"
        :aria-controls="id"
        :aria-expanded="expanded"
        @click.prevent.stop="expanded = !expanded"
      >
        {{ buttonLabel }}
      </button>
      <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 requestAnimationFrame (RAF)
          'fr-collapsing': collapsing,
        }"
        @transitionend="onTransitionEnd(expanded, focusOnExpanding)"
      >
        <component
          :is="titleTag"
          class="fr-sidemenu__title"
        >
          {{ headingTitle }}
        </component>
        <!-- @slot Slot par défaut du contenu du menu latéral -->
        <slot>
          <DsfrSideMenuList
            :id="sideMenuListId"
            :menu-items="menuItems"
            @toggle-expand="$emit('toggleExpand', $event)"
          />
        </slot>
      </div>
    </div>
  </nav>
</template>
ts
import type { RouteLocationRaw } from 'vue-router'

export type DsfrSideMenuListItemProps = { active?: boolean }

export type DsfrSideMenuProps = {
  buttonLabel?: string
  id?: string
  sideMenuListId?: string
  collapseValue?: string
  menuItems?: DsfrSideMenuListItemProps[]
  headingTitle?: string
  titleTag?: string
  focusOnExpanding?: boolean
}

export type DsfrSideMenuButtonProps = {
  active?: boolean
  expanded?: boolean
  controlId: string
}

export type DsfrSideMenuListProps = {
  id: string
  collapsable?: boolean
  expanded?: boolean
  menuItems?: (
    DsfrSideMenuListItemProps & Partial<DsfrSideMenuListProps & { to?: RouteLocationRaw, text?: string }>
    & { menuItems?: (DsfrSideMenuListItemProps & Partial<DsfrSideMenuListProps & { to?: RouteLocationRaw, text?: string }>)[] }
  )[]
  focusOnExpanding?: boolean
}