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 typestring
), reprenant celui de l’objet visé (page de destination, action, site). - un lien (prop
link
, de typestring
), sur le titre de la carte. - une image (prop
imgSrc
, de typestring
), issue ou en lien avec la page de destination à laquelle on peut ajouter une description textuelle de l'image (propaltImg
, de typestring
), 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
etendDetail
, de typestring
). - une description (prop
description
, de typestring
), 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 typeboolean
). - une zone d’action, composée de boutons (prop
buttons
, un tableau d'objets pouvant contenir les props à passer à chaque bouton (cf. le composantDsfrButton
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 delink
(string
) s'il s'agit d'un lien interne au site ou à l'application, ou dehref
(string
) s'il s'agit d'un lien externe).
Autres props :
- la taille de la carte (prop
size
, de typestring
) qui peut prendre plusieurs valeurs:md
,medium
,large
,lg
,sm
,small
. - le ratio de l'image (33%, 40% ou 50%) (prop
imgRatio
, de typestring
) qui peut prendre plusieurs valeurs:md
,medium
,large
,lg
,sm
,small
. - la balise du titre (prop
titleTag
, de typestring
) 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 typeboolean
) pour la basculer à l'horizontal. - une variante de carte indiquant que l’évènement de clic lancera un téléchargement (prop
download
, de typeboolean
).
🛠️ Props
Nom | Type | Défaut | Obligatoire | Description |
---|---|---|---|---|
title | string | ✅ | Titre de la carte | |
description | string | ✅ | Description de la carte | |
altImg | string | '' | Contenu de l’attribut alt de l’image de la carte | |
buttons | DsfrButtonProps[] | [] | Tableau de props à donner à DsfrButton | |
badges | DsfrBadgeProps[] | [] | Tableau de props à donner à DsfrBadge | |
detail | string | '' | Texte à mettre dans la première zone de détail | |
detailIcon | string | '' | Icône à mettre dans la première zone de détail (nom d’une icône @iconify/vue ou DSFR ) | |
endDetail | string | '' | Texte à mettre dans la deuxième zone de détail | |
endDetailIcon | string | '' | Icône à mettre dans la deuxième zone de détail (nom d’une icône @iconify/vue ou DSFR ) | |
download | boolean | false | Est-ce que cette carte permet de télécharger un fichier ? | |
horizontal | boolean | false | Est-ce que la carte doit être affiché avec l’image et le texte au même niveau ? | |
imgSrc | string | '' | URL vers l’image | |
link | string | '' | 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' | ||
titleTag | TitleTag | '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
<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
<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
<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>
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
}