Skip to content

En-tête - DsfrHeader

🌟 Introduction

Salut les développeurs ! Découvrez DsfrHeader, le composant d'en-tête ultra-flexible pour vos applications Vue. Conçu pour mettre en valeur votre service et vos partenaires avec style, il intègre une barre de recherche, des liens rapides, et même un emplacement pour un logo personnalisé. Préparez-vous à donner à votre application une tête bien pensée !

🏅 La documentation sur l’en-tête sur le DSFR

La story sur l’en-tête sur le storybook de VueDsfr

Structure

L’en-tête est composé :

  • du bloc Marque - obligatoire.
  • du nom de site optionnel.
  • d’une ‘baseline’ (description) sous le nom de site.
  • d’une partie fonctionnelle optionnelle - proposant des accès rapides et/ou une barre de recherche et/ou un sélecteur de langue - adaptée aux besoins particuliers de chaque site.

🛠️ Props

NomTypeDéfautObligatoireDescription
searchbarIdstring'searchbar-header'valeur de l’attribut id de l’input de la searchbar.
serviceTitlestringundefinedTitre du service affiché dans l'en-tête.
serviceDescriptionstringundefinedDescription courte du service.
homeTostring'/'Lien de la page d'accueil.
logoTextstring | string[]'Gouvernement'Texte ou texte alternatif du logo.
modelValuestring''Valeur pour la barre de recherche.
operatorImgAltstring''Texte alternatif pour l'image de l'opérateur.
operatorImgSrcstring''Source de l'image de l'opérateur.
operatorImgStyleStyleValue() => ({})Style CSS pour l'image de l'opérateur.
placeholderstring'Rechercher...'Placeholder pour la barre de recherche.
quickLinksDsfrHeaderMenuLinkProps[]() => []Liens rapides à afficher dans l'en-tête.
languageSelectorDsfrLanguageSelectorPropsundefinedLiens rapides à afficher dans l'en-tête.
searchLabelstring'Recherche'Label pour la barre de recherche.
quickLinksAriaLabelstring'Menu secondaire'Label ARIA pour les liens rapides.
showSearchbooleanfalseAffiche ou non la barre de recherche.
showBetabooleanfalseAffiche ou non l'indicateur BETA.
showSearchLabelstring'Recherche'Label du bouton pour afficher la recherche.
menuLabelstring'Menu'Label du menu.
menuModalLabelstring'Menu modal'Label du menu en mode modal.
closeMenuModalLabelstring'Fermer'Label du bouton de fermeture du menu en mode modal.
homeLabelstring'Accueil'Label de l'accueil composant le titre du lien présentant le service.

📡 Événements

NomDescriptionCharge utile
update:modelValueÉmis lors de la mise à jour de la barre de recherche.Contenu (string) du champ de saisie pour la recherche
searchÉmis lorsqu’une recherche est effectuée.Contenu (string) du champ de saisie pour la recherche
languageSelectÉmis lorsque l’utilisateur change la langue du site.Contenu (string) du champ de saisie pour la recherche

🧩 Slots

NomDescription
operatorSlot pour le logo de l'opérateur.
before-quick-linksSlot pour ajouter du contenu avant les liens rapides.
after-quick-linksSlot pour ajouter du contenu après les liens rapides.
mainnavSlot pour le menu de navigation principal.
defaultSlot par défaut pour le contenu supplémentaire dans l'en-tête.

📝 Exemples

vue
<script lang="ts" setup>
import { getCurrentInstance, ref, watch } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import type { DsfrLanguageSelectorElement } from '@/components/DsfrLanguageSelector/DsfrLanguageSelector.types'

import VIcon from '../../VIcon/VIcon.vue'
import DsfrHeader, { type DsfrHeaderProps } from '../DsfrHeader.vue'

const logoText = ['Ministère', 'de l’intérieur']
const serviceTitle = 'Nom du Site/Service'
const serviceDescription = 'baseline - précisions sur l‘organisation'
const placeholder = ''
const homeTo = '/'
const quickLinks: DsfrHeaderProps['quickLinks'] = [
  { label: 'Créer un espace', to: '/space/create', icon: 'ri-add-circle-line', iconRight: true },
  { label: 'Se connecter', to: '/login', class: 'fr-icon-user-fill' },
  { label: 'S’enregistrer', to: '/signup', icon: 'ri-account-circle-line', iconRight: true, iconAttrs: { animation: 'spin', speed: 'slow' } },
]
const languageSelector = ref({
  id: 'language-selector',
  languages: [
    { label: 'Français', codeIso: 'fr' },
    { label: 'English', codeIso: 'en' },
    { label: 'Deutsch', codeIso: 'de' },
  ],
  currentLanguage: 'fr',
})

const app = getCurrentInstance()
app?.appContext.app.use(
  createRouter({
    history: createWebHistory(),
    routes: [
      { path: '/', component: { template: '<div>Accueil</div>' } },
      { path: '/space/create', component: { template: '<div>Espace</div>' } },
      { path: '/login', component: { template: '<div>login</div>' } },
      { path: '/signup', component: { template: '<div>signup</div>' } },
    ],
  }),
).component('VIcon', VIcon)

const search = ref('')
watch(search, (newValue) => {
  console.log('search', newValue) // eslint-disable-line no-console
})

const selectLanguage = (language: DsfrLanguageSelectorElement) => {
  languageSelector.value.currentLanguage = language.codeIso
}
</script>

<template>
  <!-- Attention, il faut au moins vue 3.4 pour les props raccourcies -->
  <!-- cf. https://blog.vuejs.org/posts/vue-3-4#v-bind-same-name-shorthand -->
  <DsfrHeader
    :logo-text
    :service-title
    :service-description
    :placeholder
    :home-to
    :quick-links
    :language-selector
    @language-select="selectLanguage($event)"
  />
</template>

Exemple plus complet sur l’application de demo (dont le code source est disponible ici).

⚙️ Code source du composant

vue
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, provide, ref, toRef, useSlots } from 'vue'

import DsfrLanguageSelector, { type DsfrLanguageSelectorElement } from '../DsfrLanguageSelector/DsfrLanguageSelector.vue'
import DsfrLogo from '../DsfrLogo/DsfrLogo.vue'
import DsfrSearchBar from '../DsfrSearchBar/DsfrSearchBar.vue'
import DsfrHeaderMenuLinks from './DsfrHeaderMenuLinks.vue'
import { registerNavigationLinkKey } from './injection-key'

import type { DsfrHeaderProps } from './DsfrHeader.types'

export type { DsfrHeaderProps }

const props = withDefaults(defineProps<DsfrHeaderProps>(), {
  searchbarId: 'searchbar-header',
  languageSelector: undefined,
  serviceTitle: undefined,
  serviceDescription: undefined,
  homeTo: '/',
  logoText: () => 'Gouvernement',
  modelValue: '',
  operatorImgAlt: '',
  operatorImgSrc: '',
  operatorImgStyle: () => ({}),
  placeholder: 'Rechercher...',
  quickLinks: () => [],
  searchLabel: 'Recherche',
  quickLinksAriaLabel: 'Menu secondaire',
  showSearchLabel: 'Recherche',
  menuLabel: 'Menu',
  menuModalLabel: 'Menu modal',
  closeMenuModalLabel: 'Fermer',
  homeLabel: 'Accueil',
})

const emit = defineEmits<{
  (e: 'update:modelValue', payload: string): void
  (e: 'search', payload: string): void
  (e: 'language-select', payload: DsfrLanguageSelectorElement): void
}>()

const languageSelector = toRef(props, 'languageSelector')

const menuOpened = ref(false)
const searchModalOpened = ref(false)
const modalOpened = ref(false)

const hideModal = () => {
  modalOpened.value = false
  menuOpened.value = false
  searchModalOpened.value = false
  document.getElementById('button-menu')?.focus()
}
const onKeyDown = (e: KeyboardEvent) => {
  if (e.key === 'Escape') {
    hideModal()
  }
}

onMounted(() => {
  document.addEventListener('keydown', onKeyDown)
})
onUnmounted(() => {
  document.removeEventListener('keydown', onKeyDown)
})

const showMenu = () => {
  modalOpened.value = true
  menuOpened.value = true
  searchModalOpened.value = false
  document.getElementById('close-button')?.focus()
}
const showSearchModal = () => {
  modalOpened.value = true
  menuOpened.value = false
  searchModalOpened.value = true
}
const onQuickLinkClick = hideModal

const slots = useSlots()
const isWithSlotOperator = computed(() => Boolean(slots.operator?.().length) || !!props.operatorImgSrc)
const isWithSlotNav = computed(() => Boolean(slots.mainnav))
provide(registerNavigationLinkKey, () => {
  return hideModal
})
</script>

<template>
  <header
    role="banner"
    class="fr-header"
  >
    <div class="fr-header__body">
      <div class="fr-container  width-inherit">
        <div class="fr-header__body-row">
          <div class="fr-header__brand  fr-enlarge-link">
            <div class="fr-header__brand-top">
              <div class="fr-header__logo">
                <RouterLink
                  :to="homeTo"
                  :title="`${homeLabel} - ${serviceTitle}`"
                >
                  <DsfrLogo
                    :logo-text="logoText"
                    data-testid="header-logo"
                  />
                </RouterLink>
              </div>
              <div
                v-if="isWithSlotOperator"
                class="fr-header__operator"
              >
                <!-- @slot Slot nommé operator pour le logo opérateur. Sera dans `<div class="fr-header__operator">` -->
                <slot name="operator">
                  <img
                    v-if="operatorImgSrc"
                    class="fr-responsive-img"
                    :src="operatorImgSrc"
                    :alt="operatorImgAlt"
                    :style="operatorImgStyle"
                  >
                </slot>
              </div>
              <div
                v-if="showSearch || isWithSlotNav || quickLinks?.length"
                class="fr-header__navbar"
              >
                <button
                  v-if="showSearch"
                  class="fr-btn  fr-btn--search"
                  aria-controls="header-search"
                  :aria-label="showSearchLabel"
                  :title="showSearchLabel"
                  :data-fr-opened="searchModalOpened"
                  @click.prevent.stop="showSearchModal()"
                />
                <button
                  v-if="isWithSlotNav || quickLinks?.length"
                  id="button-menu"
                  class="fr-btn--menu  fr-btn"
                  :data-fr-opened="showMenu"
                  aria-controls="header-navigation"
                  aria-haspopup="menu"
                  :aria-label="menuLabel"
                  :title="menuLabel"
                  data-testid="open-menu-btn"
                  @click.prevent.stop="showMenu()"
                />
              </div>
            </div>
            <div
              v-if="serviceTitle"
              class="fr-header__service"
            >
              <RouterLink
                :to="homeTo"
                :title="`${homeLabel} - ${serviceTitle}`"
                v-bind="$attrs"
              >
                <p class="fr-header__service-title">
                  {{ serviceTitle }}
                  <span
                    v-if="showBeta"
                    class="fr-badge fr-badge--sm fr-badge--green-emeraude"
                  >
                    BETA
                  </span>
                </p>
              </RouterLink>
              <p
                v-if="serviceDescription"
                class="fr-header__service-tagline"
              >
                {{ serviceDescription }}
              </p>
            </div>
            <div
              v-if="!serviceTitle && showBeta"
              class="fr-header__service"
            >
              <p class="fr-header__service-title">
                <span class="fr-badge fr-badge--sm fr-badge--green-emeraude">BETA</span>
              </p>
            </div>
          </div>
          <div class="fr-header__tools">
            <div
              v-if="quickLinks?.length || languageSelector"
              class="fr-header__tools-links"
            >
              <DsfrHeaderMenuLinks
                v-if="!menuOpened"
                :links="quickLinks"
                :nav-aria-label="quickLinksAriaLabel"
              />
              <template v-if="languageSelector">
                <DsfrLanguageSelector
                  v-bind="languageSelector"
                  @select="emit('language-select', $event)"
                />
              </template>
            </div>
            <div
              v-if="showSearch"
              class="fr-header__search  fr-modal"
            >
              <DsfrSearchBar
                :searchbar-id="searchbarId"
                :label="searchLabel"
                :model-value="modelValue"
                :placeholder="placeholder"
                style="justify-content: flex-end"
                @update:model-value="emit('update:modelValue', $event)"
                @search="emit('search', $event)"
              />
            </div>
          </div>
        </div>
        <div
          v-if="showSearch || isWithSlotNav || (quickLinks && quickLinks.length) || languageSelector"
          id="header-navigation"
          class="fr-header__menu  fr-modal"
          :class="{ 'fr-modal--opened': modalOpened }"
          :aria-label="menuModalLabel"
          role="dialog"
          aria-modal="true"
        >
          <div class="fr-container">
            <button
              id="close-button"
              class="fr-btn fr-btn--close"
              aria-controls="header-navigation"
              data-testid="close-modal-btn"
              @click.prevent.stop="hideModal()"
            >
              {{ closeMenuModalLabel }}
            </button>
            <div class="fr-header__menu-links">
              <template v-if="languageSelector">
                <DsfrLanguageSelector
                  v-bind="languageSelector"
                  @select="languageSelector.currentLanguage = $event.codeIso"
                />
              </template>
              <nav role="navigation">
                <DsfrHeaderMenuLinks
                  v-if="menuOpened"
                  role="navigation"
                  :links="quickLinks"
                  :nav-aria-label="quickLinksAriaLabel"
                  @link-click="onQuickLinkClick"
                />
              </nav>
            </div>

            <template v-if="modalOpened">
              <slot
                name="mainnav"
                :hidemodal="hideModal"
              />
            </template>
            <div
              v-if="searchModalOpened"
              class="flex justify-center items-center"
            >
              <DsfrSearchBar
                :searchbar-id="searchbarId"
                :model-value="modelValue"
                :placeholder="placeholder"
                @update:model-value="emit('update:modelValue', $event)"
                @search="emit('search', $event)"
              />
            </div>
          </div>
        </div>
        <!-- @slot Slot par défaut pour le contenu du fieldset (sera dans `<div class="fr-header__body-row">`) -->
        <slot />
      </div>
    </div>
    <div class="fr-header__menu fr-modal">
      <div
        v-if="isWithSlotNav && !modalOpened"
        class="fr-container"
      >
        <!-- @slot Slot nommé mainnav pour le menu de navigation principal -->
        <slot
          name="mainnav"
          :hidemodal="hideModal"
        />
      </div>
    </div>
  </header>
</template>
ts
import type { HTMLAttributes, StyleValue } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type { DsfrLanguageSelectorProps } from '../DsfrLanguageSelector/DsfrLanguageSelector.types'
import type VIcon from '../VIcon/VIcon.vue'

export type DsfrHeaderMenuLinkProps = {
  button?: boolean
  icon?: string | InstanceType<typeof VIcon>['$props']
  iconAttrs?: InstanceType<typeof VIcon>['$props'] & HTMLAttributes
  iconRight?: boolean
  label?: string
  target?: string
  onClick?: ($event: MouseEvent) => void
  to?: RouteLocationRaw
  /**
   * @deprecated Use the prop `to` instead
   */
  href?: string
  /**
   * @deprecated Use the prop `to` instead
   */
  path?: string
  class?: string | Record<string, string> | Array<string | Record<string, string>>
}

export type DsfrHeaderProps = {
  searchbarId?: string
  serviceTitle?: string
  serviceDescription?: string
  homeTo?: string
  logoText?: string | string[]
  modelValue?: string
  operatorImgAlt?: string
  operatorImgSrc?: string
  operatorImgStyle?: StyleValue
  placeholder?: string
  quickLinks?: DsfrHeaderMenuLinkProps[]
  languageSelector?: DsfrLanguageSelectorProps
  searchLabel?: string
  quickLinksAriaLabel?: string
  showSearch?: boolean
  showSearchLabel?: string
  showBeta?: boolean
  menuLabel?: string
  menuModalLabel?: string
  closeMenuModalLabel?: string
  homeLabel?: string
}
ts
import type { InjectionKey } from 'vue'

type RegisterNavigationLink = () => () => void

export const registerNavigationLinkKey: InjectionKey<RegisterNavigationLink> = Symbol('header')