Skip to content

Pied de page - DsfrFooter

🌟 Introduction

Le DsfrFooter est un composant Vue.js pour créer un pied de page personnalisé sur un site web. Il permet d'intégrer des logos, des liens vers des partenaires, des liens légaux, et d'autres éléments essentiels dans un pied de page.

Le pied de page est présent sur l’ensemble des pages du site. Il est situé en fin de page. Le trait bleu marque la séparation entre le corps de la page et le pied de page.

🏅 La documentation sur le pied de page sur le DSFR

La story sur le pied de page sur le storybook de VueDsfr

📐 Structure

Ce composant se structure en plusieurs parties, incluant :

  1. Le Haut du Pied de Page: Peut être personnalisé avec des slots pour les listes de liens.
  2. Le Corps du Pied de Page: Contient la marque, des descriptions et des liens vers l'écosystème.
  3. Les Partenaires: Gérés par le composant DsfrFooterPartners.
  4. Le Bas du Pied de Page: Inclut les liens obligatoires et la licence.

🛠️ Props

nomtypedéfautobligatoire
a11yCompliancestring'non conforme'
a11yComplianceLinkimport('vue-router').RouteLocationRaw/a11y
legalLinkstring/mentions-legales
homeLinkimport('vue-router').RouteLocationRaw/
homeTitlestringRetour à l’accueil
partnersDsfrFooterPartnersPropsundefined
personalDataLinkstring/donnees-personnelles
cookiesLinkstring/cookies
logoTextstring | string[]() => ['République', 'Française']
descTextstringundefined
beforeMandatoryLinksDsfrFooterLinkProps[]() => []
afterMandatoryLinksDsfrFooterLinkProps[]() => []
mandatoryLinks{label: string, to: import('vue-router').RouteLocationRaw | undefined}[]Dynamique (voir script)
ecosystemLinks{label: string, href: string}[]Dynamique (voir script)
operatorLinkTextstring'Revenir à l’accueil'
operatorToimport('vue-router').RouteLocationRaw | undefined/
operatorImgStyleimport('vue').StyleValueundefined
operatorImgSrcstringundefined
operatorImgAltstring''
licenceTostring'https://github.com/etalab/licence-ouverte/blob/master/LO.md'
licenceLinkProps{ href: string } | { to: import('vue-router').RouteLocationRaw | undefined }undefined
licenceTextstring'Sauf mention contraire, tous les textes de ce site sont sous'
licenceNamestring'licence etalab-2.0'

Des boutons après la liste de liens

Vous pouvez donc insérer un bouton après la liste de liens obligatoires (ou avant dans beforeMandatoryLink) en ajoutant un élément avec un contenu similaire à celui-ci :

ts
const afterMandatoryLinks = [
  // (...)
  {
    label: 'Paramètres d’affichage',
    button: true,
    class: 'fr-icon-theme-fill fr-link--icon-left fr-px-2v',
    to: '/settings',
    onclick: () => console.log('Settings'),
  },
  // (...)
]

C’est le cas dans l’exemple.

📡 Événements

Aucun événement spécifique pour ce composant.

🧩 Slots

  1. footer-link-lists : Permet de personnaliser les listes de liens dans la partie supérieure du pied de page.
  2. description : Pour personnaliser la description dans le corps du pied de page.

📝 Exemple

vue
<script lang="ts" setup>
import VIcon from '../../VIcon/VIcon.vue'
import { createWebHistory, createRouter } from 'vue-router'
import { getCurrentInstance } from 'vue'

import DsfrFooter from '../DsfrFooter.vue'
import { useScheme } from '@/composables'

const { setScheme, theme } = useScheme()

const changeTheme = () => {
  setScheme(theme.value === 'light' ? 'dark' : 'light')
}

const beforeMandatoryLinks = [{ label: 'Before', to: '/before' }]
const afterMandatoryLinks = [
  { label: 'After', to: '/after' },
  {
    label: 'Paramètres d’affichage',
    button: true,
    class: 'fr-icon-theme-fill fr-link--icon-left fr-px-2v',
    to: '/settings',
    onclick: changeTheme,
  },
]
const a11yCompliance = 'partiellement conforme'
const logoText = ['République', 'des châtons']
const legalLink = '/mentions-legales'
const personalDataLink = '/donnees-personnelles'
const cookiesLink = '/cookies'
const a11yComplianceLink = '/a11y-conformite'
const descText = 'Description'
const homeLink = '/'
const licenceText = undefined
const licenceTo = undefined
const licenceName = undefined
const licenceLinkProps = undefined
const ecosystemLinks = [
  {
    label: 'legifrance.gouv.fr',
    href: 'https://legifrance.gouv.fr',
  },
  {
    label: 'info.gouv.fr',
    href: 'https://info.gouv.fr',
  },
  {
    label: 'service-public.fr',
    href: 'https://service-public.fr',
  },
  {
    label: 'data.gouv.fr',
    href: 'https://data.gouv.fr',
  },
]

const app = getCurrentInstance()
app?.appContext.app.use(
  createRouter({
    history: createWebHistory(),
    routes: [
      { path: '/', component: { template: '<div>Accueil</div>' } },
      { path: '/a11y-conformite', component: { template: '<div>Conformité RGAA</div>' } },
      { path: '/mentions-legales', component: { template: '<div>Mentions légales</div>' } },
      { path: '/donnees-personnelles', component: { template: '<div>Données personnelles</div>' } },
      { path: '/cookies', component: { template: '<div>cookies</div>' } },
      { path: '/after', component: { template: '<div>after</div>' } },
      { path: '/before', component: { template: '<div>before</div>' } },
      { path: '/_frame', component: { template: '<div>frame</div>' } },
    ],
  }),
).component('VIcon', VIcon)
</script>

<template>
  <DsfrFooter
    :before-mandatory-links="beforeMandatoryLinks"
    :after-mandatory-links="afterMandatoryLinks"
    :a11y-compliance="a11yCompliance"
    :logo-text="logoText"
    :legal-link="legalLink"
    :personal-data-link="personalDataLink"
    :cookies-link="cookiesLink"
    :a11y-compliance-link="a11yComplianceLink"
    :desc-text="descText"
    :home-link="homeLink"
    :licence-text="licenceText"
    :licence-to="licenceTo"
    :licence-name="licenceName"
    :licence-link-props="licenceLinkProps"
    :ecosystem-links="ecosystemLinks"
  />
</template>

⚙️ Code source du composant

vue
<script setup lang="ts">
import { computed, useSlots } from 'vue'
import type { RouteLocationRaw, RouterLink } from 'vue-router'

import DsfrLogo from '../DsfrLogo/DsfrLogo.vue'
import DsfrFooterPartners from '../DsfrFooter/DsfrFooterPartners.vue'
import DsfrFooterLink from '../DsfrFooter/DsfrFooterLink.vue'

import type { DsfrFooterProps } from './DsfrFooter.types'

export type { DsfrFooterProps }
export type {
  DsfrFooterLinkProps,
  DsfrFooterLinkListProps,
  DsfrFooterPartnersProps,
  DsfrFooterPartner,
} from './DsfrFooter.types'

const props = withDefaults(defineProps<DsfrFooterProps>(), {
  a11yCompliance: 'non conforme',
  a11yComplianceLink: '/a11y',
  legalLink: '/mentions-legales',
  homeLink: '/',
  homeTitle: 'Retour à l’accueil',
  // @ts-expect-error this is really undefined
  partners: () => undefined,
  personalDataLink: '/donnees-personnelles',
  cookiesLink: '/cookies',
  logoText: () => ['République', 'Française'],
  descText: undefined,
  beforeMandatoryLinks: () => [],
  afterMandatoryLinks: () => [],
  mandatoryLinks: (props) => [
    {
      label: `Accessibilité : ${props.a11yCompliance}`,
      to: props.a11yComplianceLink,
    },
    {
      label: 'Mentions légales',
      to: props.legalLink,
      'data-testid': '/mentions-legales',
    },
    {
      label: 'Données personnelles',
      to: props.personalDataLink,
    },
    {
      label: 'Gestion des cookies',
      to: props.cookiesLink,
    },
  ],
  ecosystemLinks: () => [
    {
      label: 'info.gouv.fr',
      href: 'https://info.gouv.fr',
    },
    {
      label: 'service-public.fr',
      href: 'https://service-public.fr',
    },
    {
      label: 'legifrance.gouv.fr',
      href: 'https://legifrance.gouv.fr',
    },
    {
      label: 'data.gouv.fr',
      href: 'https://data.gouv.fr',
    },
  ],
  operatorLinkText: 'Revenir à l’accueil',
  operatorTo: '/',
  operatorImgStyle: undefined,
  operatorImgSrc: undefined,
  operatorImgAlt: '',
  licenceText: 'Sauf mention explicite de propriété intellectuelle détenue par des tiers, les contenus de ce site sont proposés sous',
  licenceTo: 'https://github.com/etalab/licence-ouverte/blob/master/LO.md',
  // @ts-expect-error this is really undefined
  licenceLinkProps: () => undefined,
  licenceName: 'licence etalab-2.0',
})

const allLinks = computed(() => {
  return [
    ...props.beforeMandatoryLinks,
    ...props.mandatoryLinks,
    ...props.afterMandatoryLinks,
  ]
})

const slots = useSlots()
const isWithSlotLinkLists = computed(() => {
  return slots['footer-link-lists']?.().length
})
const isExternalLink = computed(() => {
  const to = props.licenceTo || (props.licenceLinkProps as { to: RouteLocationRaw }).to
  return to && typeof to === 'string' && to.startsWith('http')
})
const routerLinkLicenceTo = computed(() => {
  return isExternalLink.value ? '' : props.licenceTo
})
const aLicenceHref = computed(() => {
  return isExternalLink.value ? props.licenceTo : ''
})

const externalOperatorLink = computed(() => {
  return typeof props.operatorTo === 'string' && props.operatorTo.startsWith('http')
})
</script>

<template>
  <footer
    id="footer"
    class="fr-footer"
    role="contentinfo"
  >
    <div
      v-if="isWithSlotLinkLists"
      class="fr-footer__top"
    >
      <div class="fr-container">
        <div class="fr-grid-row fr-grid-row--start fr-grid-row--gutters">
          <!-- @slot Slot #footer-link-lists pour pouvoir changer les liens dans la rubrique en haut du pied de page  -->
          <slot name="footer-link-lists" />
        </div>
      </div>
    </div>
    <div class="fr-container">
      <div class="fr-footer__body">
        <div
          v-if="operatorImgSrc"
          class="fr-footer__brand fr-enlarge-link"
        >
          <DsfrLogo
            :logo-text="logoText"
          />
          <a
            v-if="externalOperatorLink"
            :href="(operatorTo as string)"
            data-testid="card-link"
            class="fr-footer__brand-link"
          >
            <img
              class="fr-footer__logo"
              :style="operatorImgStyle"
              :src="operatorImgSrc"
              :alt="operatorImgAlt"
            >
          </a>
          <RouterLink
            v-else
            class="fr-footer__brand-link"
            :to="homeLink"
            :title="homeTitle"
          >
            <img
              class="fr-footer__logo"
              :style="operatorImgStyle"
              :src="operatorImgSrc"
              :alt="operatorImgAlt"
            >
          </RouterLink>
        </div>
        <div
          v-else
          class="fr-footer__brand fr-enlarge-link"
        >
          <RouterLink
            :to="homeLink"
            :title="homeTitle"
          >
            <DsfrLogo
              :logo-text="logoText"
            />
          </RouterLink>
        </div>
        <div class="fr-footer__content">
          <p
            class="fr-footer__content-desc"
          >
            <!-- @slot Slot #description pour le contenu de la description du footer. Sera dans `<p class="fr-footer__content-desc">` -->
            <slot name="description">
              {{ descText }}
            </slot>
          </p>
          <ul class="fr-footer__content-list">
            <li
              v-for="(link, index) in ecosystemLinks"
              :key="index"
              class="fr-footer__content-item"
            >
              <a
                class="fr-footer__content-link"
                :href="link.href"
                target="_blank"
                rel="noopener noreferrer"
              >
                {{ link.label }}
              </a>
            </li>
          </ul>
        </div>
      </div>
      <DsfrFooterPartners
        v-if="partners"
        v-bind="partners"
      />
      <div class="fr-footer__bottom">
        <ul class="fr-footer__bottom-list">
          <li
            v-for="(link, index) in allLinks"
            :key="index"
            class="fr-footer__bottom-item"
          >
            <DsfrFooterLink
              v-bind="link"
            />
          </li>
        </ul>
        <div
          v-if="licenceText"
          class="fr-footer__bottom-copy"
        >
          <p>
            {{ licenceText }}
            <component
              :is="isExternalLink ? 'a' : 'RouterLink'"
              class="fr-link-licence  no-content-after"
              :to="isExternalLink ? null : routerLinkLicenceTo"
              :href="aLicenceHref"
              :target="isExternalLink ? '_blank' : undefined"
              rel="noopener noreferrer"
              v-bind="licenceLinkProps"
            >
              {{ licenceName }}
            </component>
          </p>
        </div>
      </div>
    </div>
  </footer>
</template>

<style scoped>
.fr-footer {
  color: var(--text-default-grey);
}
.no-content-after {
  --link-blank-content: '';
}
.ov-icon {
  margin-bottom: 0;
}
</style>
ts
import type { HTMLAttributes, StyleValue } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type VIcon from '../VIcon/VIcon.vue'

export type DsfrFooterPartner = {
  href: string
  logo: string
  name: string
}

export type DsfrFooterPartnersProps = {
  mainPartner?: DsfrFooterPartner
  subPartners?: DsfrFooterPartner[]
  title?: string
}

export type DsfrFooterLinkProps = {
  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
  href?: string
}

export type DsfrFooterLinkListProps = {
  categoryName: string
  links: DsfrFooterLinkProps[]
}

export type DsfrFooterProps = {
  a11yCompliance?: string
  a11yComplianceLink?: RouteLocationRaw
  legalLink?: string
  homeLink?: RouteLocationRaw
  homeTitle?: string
  partners?: DsfrFooterPartnersProps
  personalDataLink?: string
  cookiesLink?: string
  logoText?: string | string[]
  descText?: string
  beforeMandatoryLinks?: DsfrFooterLinkProps[]
  afterMandatoryLinks?: DsfrFooterLinkProps[]
  mandatoryLinks?: { label: string, to: RouteLocationRaw | undefined }[]
  ecosystemLinks?: { label: string, href: string }[]
  operatorLinkText?: string
  operatorTo?: RouteLocationRaw | undefined
  operatorImgStyle?: StyleValue
  operatorImgSrc?: string
  operatorImgAlt?: string
  licenceTo?: string
  licenceLinkProps?: { href: string } | { to: RouteLocationRaw | undefined }
  licenceText?: string
  licenceName?: string
}