Skip to content

Carte - DsfrCard

🌟 Introduction

La carte c'est tout simplement l'indispensable pour agrémenter vos sites et applications d'amuse-bouches esthétiques vers des contenus proposés. Il s'agit d'un composant permettant un aperçu d'une page et un lien vers cette dernière. Elle fait généralement partie d'une liste menant vers des contenus similaires.

La carte existe en trois tailles (LG, MD, SM) et deux formats (horizontal et vertical) déclinés sur deux supports (desktop et mobile), vous trouverez forcément votre bonheur ! Les cartes horizontales sont réservées au desktop (en mobile, une carte horizontale devient verticale).

🏅 La documentation sur la carte sur le DSFR

La story sur la carte sur le storybook de VueDsfr

📐 Structure

Une carte digne de ce nom se compose des éléments suivants :

  • un titre (prop title, de type string), reprenant celui de l’objet visé (page de destination, action, site).
  • un lien (prop link, de type string), sur le titre de la carte.
  • une image (prop imgSrc, de type string), issue ou en lien avec la page de destination à laquelle on peut ajouter une description textuelle de l'image (prop altImg, de type string), ce texte alternatif sera affiché sur la page si l'image ne peut pas être chargée et sera très utile pour l'accessibilité.
  • deux zones de détails destinées à une icône et un texte - optionnels (props detail et endDetail, de type string).
  • une description (prop description, de type string), de 5 lignes maximum (tronquée au-delà).
  • une icône illustrative (par défaut, une flèche) - optionnelle peut se désactiver (prop noArrow, de type boolean).
  • une zone d’action, composée de boutons (prop buttons, un tableau d'objets pouvant contenir les props à passer à chaque bouton (cf. le composant DsfrButton afin de connaître les props à passer)).
  • une zone d’action, composée de liens (prop linksGroup, un tableau d'objets composé de la propriété label (string),et de link (string) s'il s'agit d'un lien interne au site ou à l'application, ou de href (string) s'il s'agit d'un lien externe).

Autres props :

  • la taille de la carte (prop size, de type string) qui peut prendre plusieurs valeurs: md, medium, large, lg, sm, small.
  • le ratio de l'image (33%, 40% ou 50%) (prop imgRatio, de type string) qui peut prendre plusieurs valeurs: md, medium, large, lg, sm, small.
  • la balise du titre (prop titleTag, de type string) afin de respecter la hiérarchie des titres. Valeurs possibles: h1, h2, h3, h4, h5, h6.
  • l'orientation de la carte (verticale par défaut) (prop horizontal, de type boolean) pour la basculer à l'horizontal.
  • une variante de carte indiquant que l’évènement de clic lancera un téléchargement (prop download, de type boolean).

🛠️ Props

NomTypeDéfautObligatoireDescription
titlestringTitre de la carte
descriptionstringDescription de la carte
altImgstring''Contenu de l’attribut alt de l’image de la carte
buttonsDsfrButtonProps[][]Tableau de props à donner à DsfrButton
badgesDsfrBadgeProps[][]Tableau de props à donner à DsfrBadge
detailstring''Texte à mettre dans la première zone de détail
detailIconstring''Icône à mettre dans la première zone de détail (nom d’une icône @iconify/vue ou DSFR)
endDetailstring''Texte à mettre dans la deuxième zone de détail
endDetailIconstring''Icône à mettre dans la deuxième zone de détail (nom d’une icône @iconify/vue ou DSFR)
downloadbooleanfalseEst-ce que cette carte permet de télécharger un fichier ?
horizontalbooleanfalseEst-ce que la carte doit être affiché avec l’image et le texte au même niveau ?
imgSrcstring''URL vers l’image
linkstring''Lien vers lequel la carte pointe
linksGroup({ label: string, to?: RouteLocationRaw, link?: string, href?: string })[][]liste de liens : objet contenant to ou href pour le lien et label avec le texte du lien
size'md'* | 'medium' | 'large' | 'lg' | 'sm' | 'small' | undefined'md'Taille de la carte
imgRatio'md' | 'medium' | 'large' | 'lg' \ 'sm' \ 'small' \ undefined'md'
titleTagTitleTag'h3'Balise du titre de la carte

🧩 Slots

start-details permet de placer une précision, sous forme de tags (cliquables ou non)

cf. DSFR : Composant - Tag ou Composant - Badge (jusqu'à 4 éléments) cf. DSFR : Composant - Carte.

📝 Exemples

📝 Exemple avec tags et badges dans l’en-tête sans actions

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

import DsfrTags from '../../DsfrTag/DsfrTags.vue'
import VICon from '../../VIcon/VIcon.vue'
import DsfrCard from '../DsfrCard.vue'
import type { DsfrBadgeProps } from '../../DsfrBadge/DsfrBadge.types'

const app = getCurrentInstance()
app?.appContext.app.component('VICon', VICon)

const link = 'https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/carte'
const description = 'Description exceptionnellement précise'
const detail = 'Détails absolument essentiels'
const detailIcon = 'Détails absolument essentiels'
const endDetail = 'Autres détails absolument essentiels'
const endDetailIcon = 'fr-icon-arrow-right-line'
const altImg = 'Un adorable quoique redoutable chaton'
const imgSrc = 'https://loremflickr.com/450/400/cat'

const exampleTags = [
  {
    label: 'Tag1',
  },
  {
    label: 'Tag2',
  },
  {
    label: 'Tag3',
  },
  {
    label: 'Tag4',
  },
]

const exampleBadges = [
  {
    label: 'Badge info',
    type: 'info',
  },
  {
    label: 'Badge success',
    type: 'success',
  },
] as DsfrBadgeProps[]
</script>

<template>
  <div class="fr-container fr-my-2v">
    <DsfrCard
      :img-src="imgSrc"
      :link="link"
      :description="description"
      :detail="detail"
      :detail-icon="detailIcon"
      :end-detail="endDetail"
      :end-detail-icon="endDetailIcon"
      :alt-img="altImg"
      :badges="exampleBadges"
      title="Titre de la carte"
      size="large"
      ratio-img="large"
    >
      <template #start-details>
        <DsfrTags
          :tags="exampleTags"
        />
      </template>
    </DsfrCard>
  </div>
</template>

📝 Exemple avec actions sans tags

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

import VICon from '../../VIcon/VIcon.vue'
import DsfrCard from '../DsfrCard.vue'

const app = getCurrentInstance()
app?.appContext.app.component('VICon', VICon)

const description = 'Description exceptionnellement précise'
const detail = 'Détails absolument essentiels'
const detailIcon = 'Détails absolument essentiels'
const endDetail = 'Autres détails absolument essentiels'
const endDetailIcon = 'fr-icon-arrow-right-line'
const altImg = 'Un adorable quoique redoutable chaton'
const imgSrc = 'https://loremflickr.com/450/400/cat'

const actions = [
  {
    label: 'Valider',
    onClick: (event: MouseEvent) => {
      event.preventDefault()
      alert('Valider') // eslint-disable-line no-alert
    },
  },
  {
    label: 'Annuler',
    secondary: true,
    onClick: (event: MouseEvent) => {
      event.preventDefault()
      alert('Annuler') // eslint-disable-line no-alert
    },
  },
]
</script>

<template>
  <div class="fr-container fr-my-2v">
    <DsfrCard
      :img-src="imgSrc"
      :description="description"
      :detail="detail"
      :detail-icon="detailIcon"
      :end-detail="endDetail"
      :end-detail-icon="endDetailIcon"
      :alt-img="altImg"
      ratio-img="large"
      size="large"
      title="Titre de la carte"
      :buttons="actions"
    />
  </div>
</template>

⚙️ Code source du composant

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

import DsfrBadge from '../DsfrBadge/DsfrBadge.vue'
import DsfrButtonGroup from '../DsfrButton/DsfrButtonGroup.vue'

import DsfrCardDetail from './DsfrCardDetail.vue'
import type { DsfrCardProps } from './DsfrCard.types'

export type { DsfrCardProps }

const props = withDefaults(defineProps<DsfrCardProps>(), {
  imgSrc: undefined,
  link: undefined,
  detail: undefined,
  detailIcon: undefined,
  endDetail: undefined,
  endDetailIcon: undefined,
  altImg: '',
  buttons: () => [],
  linksGroup: () => [],
  badges: () => [],
  titleTag: 'h3',
  size: 'md',
  imgRatio: 'md',
})

const sm = computed(() => {
  return ['sm', 'small'].includes(props.size)
})
const lg = computed(() => {
  return ['lg', 'large'].includes(props.size)
})

const smImg = computed(() => {
  return ['sm', 'small'].includes(props.imgRatio)
})
const lgImg = computed(() => {
  return ['lg', 'large'].includes(props.imgRatio)
})
const externalLink = computed(() => {
  return typeof props.link === 'string' && props.link.startsWith('http')
})

const titleElt = ref<HTMLElement | null>(null)
const goToTargetLink = () => {
  (titleElt.value?.querySelector('.fr-card__link') as HTMLDivElement).click()
}
defineExpose({ goToTargetLink })
</script>

<template>
  <div
    class="fr-card"
    :class="{
      'fr-card--horizontal': horizontal,
      'fr-enlarge-link': !noArrow,
      'fr-card--sm': sm,
      'fr-card--lg': lg,
      'fr-card--horizontal-tier': smImg,
      'fr-card--horizontal-half': lgImg,
      'fr-card--download': download,
      'fr-enlarge-button': enlarge,
    }"
    data-testid="fr-card"
  >
    <div class="fr-card__body">
      <div class="fr-card__content">
        <component
          :is="titleTag"
          class="fr-card__title"
        >
          <a
            v-if="externalLink"
            :href="(link as string)"
            data-testid="card-link"
            class="fr-card__link"
          >{{ title }}</a>
          <RouterLink
            v-else-if="link"
            :to="link"
            class="fr-card__link"
            data-testid="card-link"
            @click="$event.stopPropagation()"
          >
            {{ title }}
          </RouterLink>
          <template v-else>
            {{ title }}
          </template>
        </component>
        <p class="fr-card__desc">
          {{ description }}
        </p>
        <div
          v-if="$slots['start-details'] || detail"
          class="fr-card__start"
        >
          <slot name="start-details" />
          <DsfrCardDetail
            v-if="detail"
            :icon="detailIcon"
          >
            {{ detail }}
          </DsfrCardDetail>
        </div>
        <div
          v-if="$slots['end-details'] || endDetail"
          class="fr-card__end"
        >
          <slot name="end-details" />
          <DsfrCardDetail
            v-if="endDetail"
            :icon="endDetailIcon"
          >
            {{ endDetail }}
          </DsfrCardDetail>
        </div>
      </div>

      <div
        v-if="buttons.length || linksGroup.length"
        class="fr-card__footer"
      >
        <DsfrButtonGroup
          v-if="buttons.length"
          :buttons="buttons"
          inline-layout-when="always"
          :size="size"
          reverse
        />
        <ul
          v-if="linksGroup.length"
          class="fr-links-group"
        >
          <li
            v-for="(singleLink, i) in linksGroup"
            :key="`card-link-${i}`"
          >
            <RouterLink
              v-if="singleLink.to"
              :to="singleLink.to"
            >
              {{ singleLink.label }}
            </RouterLink>
            <a
              v-else
              :href="(singleLink.link || singleLink.href)"
              class="fr-link fr-icon-arrow-right-line fr-link--icon-right"
              :class="{
                'fr-link--sm': sm,
                'fr-link--lg': lg,
              }"
            >
              {{ singleLink.label }}
            </a>
          </li>
        </ul>
      </div>
    </div>
    <div
      v-if="imgSrc || badges.length"
      class="fr-card__header"
    >
      <div
        v-if="imgSrc"
        class="fr-card__img"
      >
        <img
          :src="imgSrc"
          class="fr-responsive-img"
          :alt="altImg"
          data-testid="card-img"
        >
        <!-- L'alternative de l'image (attribut alt) doit à priori rester vide car l'image est illustrative
          et ne doit pas être restituée aux technologies d’assistance. Vous pouvez toutefois remplir l'alternative si vous
          estimez qu'elle apporte une information essentielle à la compréhension du contenu non présente dans le texte -->
      </div>
      <ul
        v-if="badges.length"
        class="fr-badges-group"
        data-testid="card-badges"
      >
        <li
          v-for="(badge, index) in badges"
          :key="index"
        >
          <DsfrBadge v-bind="badge" />
        </li>
      </ul>
    </div>
  </div>
</template>
ts
import type { RouteLocationRaw } from 'vue-router'

import type { DsfrBadgeProps } from '../DsfrBadge/DsfrBadge.types'
import type { DsfrButtonProps } from '../DsfrButton/DsfrButton.types'
import type VIcon from '../VIcon/VIcon.vue'

export type DsfrCardDetailProps = {
  icon?: string | InstanceType<typeof VIcon>['$props']
}

export type DsfrCardProps = {
  imgSrc?: string
  link?: string | RouteLocationRaw
  title: string
  description: string
  size?: 'md' | 'medium' | 'large' | 'lg' | 'sm' | 'small' | undefined
  detail?: string
  detailIcon?: DsfrCardDetailProps['icon']
  endDetail?: string
  endDetailIcon?: DsfrCardDetailProps['icon']
  altImg?: string
  titleTag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
  badges?: DsfrBadgeProps[]
  buttons?: DsfrButtonProps[]
  imgRatio?: 'md' | 'medium' | 'lg' | 'large' | 'sm' | 'small'
  linksGroup?: {
    label: string
    to?: RouteLocationRaw
    /** @deprecated utiliser href à la place, link sera supprimé dans une version future */
    link?: string
    href?: string
  }[]
  noArrow?: boolean
  horizontal?: boolean
  download?: boolean
  enlarge?: boolean
}