Affiche une valeur clé (KPI) avec formatage, couleurs conditionnelles basees sur des seuils, et icones optionnelles.

Attributs

AttributTypeDescription
sourceStringId de la source (ou du transformateur) dont ce KPI consomme les données. Facultatif si value est un littéral (value="=667").
valueStringExpression de valeur — convention cible anglaise (#300). Grammaire commune "champ:fn" (#303), ex. value="population:sum". champ:distinct (alias count-distinct, #672) : nombre de valeurs distinctes, null et chaîne vide exclus, calculé sur les lignes reçues. meta:total (#659) : total publié par l'amont (total serveur en server-side, lignes avant limit derrière un query) — count ne compte que les lignes reçues. Total inconnu de l'amont (page serveur ou lot tronqué sans total, #1046) : « — ». Ratio (#673) : value="count:statut:ouvert / count", chaque côté dans la grammaire ci-dessus (meta:total compris). Résultat = fraction (0,35) ; format="pourcentage" la rend en pourcentage (35 %) — les seuils s'expriment alors en pourcentage aussi. Division par zéro : « — ». count:champ:valeur accepte un champ tableau (un élément égal suffit). Depuis #953, le where ci-dessous et le filtre entre accolades count{tags:eq:urgent} font PAREIL : value="count:tags:urgent" et value="count{tags:eq:urgent}" rendent le MÊME chiffre. L'asymétrie de #842 — deux chiffres sur le même jeu, et c'était « voulu » — a disparu : l'égalité client est alignée sur celle du portail, qui lit déjà = sur un champ multivalué comme un « contient » (mesuré le 2026-09-19). Seul count accepte une valeur de filtre : sum:champ:valeur est une erreur de configuration (#764). Filtre propre à une expression (#776), dialecte du where entre accolades : value="effectif:sum{sexe:eq:F} / effectif:sum" rend une part de SOMMES — le filtre ne vaut que pour son côté du ratio, là où where filtre les deux. Marche aussi pour count{…} et les autres fonctions ; un filtre non reconnu est une erreur de configuration. Le contenu des accolades est lu d'un bloc : une valeur qui contient / ne coupe pas le ratio (#839). champ:first / champ:last : valeur du champ sur la première / la dernière ligne, DANS L'ORDRE COURANT — poser un order-by en amont (ex. dernière valeur d'une série datée). Propres au KPI : absentes de l'aggregate de dsfr-data-query. champ:evolution (#675) : (dernière − première) / première sur les lignes DANS LEUR ORDRE COURANT — poser un order-by chronologique en amont. Fraction, rendue en pourcentage par format="pourcentage", trend et lines ; « — » si moins de deux valeurs ou première = 0.
headingStringTitre affiché AU-DESSUS de la valeur (surtitre, style majuscules grises). Nommé heading et non title : ce dernier entrerait en collision avec la propriété DOM native HTMLElement.title (infobulle).
labelStringLibellé affiché sous le chiffre (et sous les lines)
descriptionStringDescription détaillée pour l'accessibilité
trendStringRACCOURCI HERITE — pour une ligne d'evolution riche (signe, suffixe, couleur, repli n.d.), preferez lines. Conserve pour compatibilite. Expression d'agrégation pour la tendance, évaluée sur les données de la source (grammaire commune "champ:fn", ex. "evolution:avg") — PAS un litteral : l'ancienne doc ("+3.2") laissait croire qu'on passait une valeur, la chaîne etait interpretee comme nom de champ (#303). Rendue avec une fleche (↑/↓) en pourcentage fr-FR ("↑ 5,2 %"). trend="recettes:evolution" (#675) : taux d'évolution entre la première et la dernière ligne, rendu en pourcentage.
linesStringLignes secondaires declaratives (JSON), rendues ENTRE la valeur et le label. Chaque item est soit data-driven (value = expression "champ:fn"), soit texte statique (text), avec couleur declarative. Ex. [{"value":"evol:avg","sign":true,"suffix":"vs mai 2025","color":"auto"}]. Schema complet : packages/core/src/utils/kpi-lines.ts (KpiLineSpec).
formatFormatTypeFormat d'affichage : nombre (défaut), pourcentage, euro, decimal, compact (14 785 684 → « 14,8 M »), date (chaîne ISO → « 09/09/2026 », #667). Les décimales passent par decimals, jamais par le format (euro:3 est refusé et affiché comme erreur de configuration, #665).
unitStringUnité accolée après la valeur (espace insécable), ex. format="compact" unit="€" → « 44,9 Md € ». Surtout utile avec nombre, decimal et compact — euro et pourcentage portent déjà leur symbole (#665).
decimalsNumberNombre de décimales affichées (entier 0 à 20), ex. format="euro" decimals="3" → « 1,749 € ». Fixe pour nombre, pourcentage, euro et decimal ; plafond pour compact ; sans effet sur date. Absent : défaut historique du format (#665).
iconStringClasse d'icône DSFR (fr-icon-leaf-line) ou Remix (ri-global-line). Une CLASSE, jamais du balisage : la valeur doit suivre ^(fr-icon|ri)-[a-z0-9-]+$, sinon elle est ignorée avec un avertissement console qui la nomme (une fois par valeur). Placement et taille : icon-position, icon-size. Remplacée par picto si les deux sont posés.
icon-positionIconPosition | stringOù se place l'icône (ou le pictogramme) : label (défaut, rendu historique : entre le surtitre et la valeur, en gris), top (en tête de la carte, dans la couleur de l'accent — vignette 1a), right (à droite, alignée en haut, le texte garde sa marge — vignette 1b). Sans effet sans icon ni picto.
icon-sizeIconSize | stringTaille de l'icône : sm = 1,5 rem (24 px — sans attribut, c'est le rendu historique, inchangé) ou md = 2 rem (32 px). L'échelle s'arrête là : l'échelle documentée du DSFR s'arrête à fr-icon--lg = 2 rem, et au-delà le DSFR ne parle plus d'icône mais de PICTOGRAMME — pour une illustration de 48 ou 80 px, poser picto, pas une icône agrandie. Ni lg, ni valeur en pixels : une autre valeur est ignorée avec un avertissement. Vaut aussi pour picto, sur l'échelle des tuiles DSFR : sm = 3,5 rem, md = 5 rem (sans attribut : md, la tuile DSFR standard). Pour une icône fr-icon-*, la taille passe par --icon-size (le glyphe est un ::before masqué, indifférent à font-size).
pictoStringPictogramme DSFR illustratif (fr-artwork), par son NOM : environment/leaf, buildings/city-hall… (dossier de catégorie + fichier, sans .svg). Contraint à ^[a-z0-9-]+(/[a-z0-9-]+)*$ — ce motif exclut ../ et tout schéma sans assainisseur. Le composant rend le SVG canonique à trois <use> (#artwork-decorative, #artwork-minor, #artwork-major) dont l'adresse est picto-base + nom + .svg : picto-base est OBLIGATOIRE (sans lui, rien n'est rendu, avec un avertissement). Les couleurs viennent des classes fr-artwork-* du DSFR — le mode sombre suit sans travail — et une couleur illustrative (color-token) est reportée en fr-artwork--<nom>. ⚠️ <use href> vers un AUTRE domaine n'est pas rendu par les navigateurs (pas de CORS sur use) : picto-base doit servir les SVG depuis l'origine de la page (copie locale de dist/artwork/pictograms/), pas depuis un CDN. Prime sur icon. Mêmes icon-position et icon-size que l'icône.
picto-fieldStringMême chose que picto, mais le nom est lu dans un CHAMP de la première ligne reçue (picto-field="theme_picto") — utile dans un répéteur. Même motif, même refus. picto prime s'il est posé.
picto-baseStringPréfixe d'adresse des pictogrammes, écrit par l'intégrateur : picto-base="/dsfr/artwork/pictograms/". Le nom (picto) y est concaténé (barre finale ajoutée si absente). C'est ce découpage nom / base qui rend picto sûr par construction. Même origine que la page, voir picto.
imageStringImage libre (photo, logo) par son URL. Passée par la même liste blanche de schémas que le format {{champ:url}} des gabarits (http:, https:, mailto:, tel: ou relative) : une URL refusée (javascript:, data:…) n'affiche rien et avertit. Placement : image-position. Texte alternatif : image-alt (vide = décorative). Rendue seulement quand la donnée est là.
image-altStringTexte alternatif de image. Vide (défaut) : image décorative (alt="").
image-positionImagePosition | stringPlacement de image : top (défaut, bandeau 16:9 bord à bord au-dessus du contenu — vignette 1d), left (colonne de 10 rem pleine hauteur, le liseré reste à gauche de l'image — 2a), right (vignette carrée de 7,5 rem dans la marge, à droite du texte — 2b).
orientationhorizontal · verticalhorizontal (défaut, carte à liseré gauche) ou vertical : tuile à liseré HAUT (sauf border explicite). Avec une icône, un pictogramme ou une image, tout passe au-dessus du surtitre et le texte est centré (vignette 1f) ; sans média, le KPI reste aligné à gauche — la version sobre des chiffres-clés éditoriaux (1g). Sur dsfr-data-kpi-group, le même attribut empile les KPI (voir le groupe).
borderBorderMode | stringTracé du liseré ; sa couleur reste celle de color-token ou des seuils. left (défaut, 4 px, rendu historique), top (4 px), bottom (filet de 2 px, comme les champs DSFR), outline (contour de 1 px), left-short (4 px à hauteur de la valeur), none. Autre valeur : ignorée, avertissement.
tintStringFond teinté dans la couleur du token : tint (ou tint="true") prend le fond 950 (--background-contrast-<nom>) ; tint="975" le fond le plus clair (--background-alt-<nom>) ; tint="925" le plus soutenu (--<nom>-925-125). Les 4 tokens sémantiques n'ont pas de 925 dans le DSFR : replié sur 950 avec un avertissement. La VALEUR reste en gris titre (--text-title-grey) : les teintes pleines claires (tournesol, café-crème, galet) ne tiennent pas le contraste pour du texte — planche 3b. Le surtitre et le libellé passent en --text-default-grey pour la même raison. Se combine avec border (souvent border="none").
color-tokenKpiColor | ''Couleur forcée. Deux familles, deux SENS : - les 4 tokens sémantiques vert, orange, rouge, bleu disent un ÉTAT (bon, attention, critique, neutre) — c'est aussi ce que posent les seuils, et ce que le libellé accessible annonce (« etat bon ») ; - les 17 couleurs illustratives DSFR (green-emeraude, blue-cumulus, purple-glycine, orange-terre-battue… liste : ILLUSTRATIVE_COLOR_TOKENS) disent une CATÉGORIE — thème, ministère, famille de données — et n'annoncent aucun état. Un KPI en rouge illustratif (pink-tuile) qui ne veut pas dire « mauvais » est un contresens de lecture : l'état reste aux tokens sémantiques. Une couleur illustrative pose le liseré et l'icône en teinte pleine via var(--border-plain-<nom>) et, avec tint, le fond via var(--background-contrast-<nom>) — tokens DSFR existants, aucun hexadécimal : le mode sombre suit. Nom inconnu : ignoré avec avertissement, repli sur les seuils puis bleu.
threshold-greenNumberSeuil au-dessus duquel la valeur est verte
threshold-orangeNumberSeuil au-dessus duquel la valeur est orange
spanStringLargeur sur la grille de 12 colonnes (1-12), dans un <dsfr-data-kpi-group> : span="6" occupe la moitié de la ligne. Remplace col, même sens (#790) ; prime sur col s'ils sont posés ensemble. Sans valeur par défaut, et pour la même raison que col : la propriété est reflétée, donc une valeur initiale '' poserait span="" sur CHAQUE KPI. La largeur par défaut du groupe est portée par une règle ::slotted(*:not([col]):not([span])) — un attribut vide, mais présent, la désactive et tous les KPI retombent en grid-column: auto (#822).
colNumberLargeur en colonnes DSFR (1-12). Significatif uniquement dans un <dsfr-data-kpi-group>. Même rôle que span, qui est préféré (#790) ; toujours accepté.
whereStringFiltre des lignes AVANT le calcul (#674), dialecte colon de dsfr-data-query : where="categorie:eq:Actif, montant:gte:1000" — mêmes 12 opérateurs (eq, neq, gt, gte, lt, lte, contains, notcontains, in, notin, isnull, isnotnull), même égalité lâche, chemins imbriqués acceptés. Appliqué à value, trend et lines. CÔTÉ CLIENT SEULEMENT : le KPI ne délègue rien au serveur, le filtre porte sur les lignes reçues (derrière un limit ou une page, poser le where sur la source ou une query amont). meta:total n'en tient pas compte. Une clause non reconnue est une erreur de configuration. CHAMP TABLEAU (#953, ex-#842) : eq / in regardent DANS le tableau. where="tags:eq:urgent" retient une ligne dont tags vaut ['urgent','social'], exactement comme value="count:tags:urgent" la compte, et comme le portail la retiendrait sur une clause déléguée. Le repli textuel est gardé en OU : ['a','b'] matche encore 'a,b' côté client, là où le portail rend 0 — le client ne peut donc que gagner des lignes, jamais en perdre. neq / notin, étant la négation, en perdent (le portail aussi : son != est la négation stricte de son =). ⚠️ Le KPI ne délègue jamais : son where évalue toujours la voie client. Depuis l'alignement, c'est sans conséquence sur un champ tableau — un KPI et un graphique portant le même where rendent le même chiffre, au repli textuel près. Le booléen dérivé en amont (dsfr-data-normalize compute="a_urgent = when contains(tags,'urgent') then 1 else 0", puis where="a_urgent:eq:1") reste valide et garde un intérêt — le filtre final porte sur un scalaire, donc regroupable et délégable — mais il n'est plus NÉCESSAIRE.
idle-messageStringMessage rendu quand l'amont attend un filtre (require-where, #690). Distinct de « aucune donnée » : aucune requête n'a été faite. Vide, le libellé par défaut est utilisé.

Les anciens noms francais (valeur, icone, couleur, seuil-vert, seuil-orange, tendance) restent lus en alias deprecies (#300).

En attente d'un filtre

Un chiffre est le pire endroit où afficher un zéro qui n'en est pas un. Quand l'amont porte require-where (<dsfr-data-source> ou <dsfr-data-query>), aucune requête n'est encore partie : le KPI rend le texte de idle-message (« Choisissez un filtre pour afficher les données » par défaut) au lieu d'une valeur. Distinct de « aucune donnée », qui signalerait une requête revenue vide. Le mécanisme est décrit sur dsfr-data-source.

dsfr-data-kpi-group - Attributs

AttributTypeDescription
aria-labelStringLabel accessible du groupe
per-rowStringNombre de KPI par ligne à partir de 768 px (en dessous : un par ligne) — 1, 2, 3, 4, 6 ou 12, les diviseurs de la grille. Échelle mobile-first (#789) : per-row="2 md:4" — deux KPI par ligne sur téléphone, quatre à partir de 768 px ; avec un terme de base ou un palier sm, le repli forcé sur une colonne ne s'applique plus. Remplace cols, même sens (#790) ; prime sur cols s'ils sont posés ensemble. Un KPI qui porte span (ou col) garde sa propre largeur.
colsNumberNombre de KPI par ligne par défaut (1-12). Chaque enfant occupe Math.floor(12/cols) colonnes. Même rôle que per-row, qui est préféré : cols désigne une LARGEUR sur dsfr-data-facets (#790). Toujours accepté, avec le même sens.
gapsm · md · lgEspacement entre KPIs : sm (0.5rem), md (1rem), lg (1.5rem)
orientationhorizontal · verticalhorizontal (défaut : la grille 12 colonnes) ou vertical : les KPI sont EMPILÉS en colonne dans un seul cadre — un liseré gauche continu porté par le groupe, un filet entre les items, valeur à 2 rem, icône ou pictogramme à 2,5 rem à gauche du texte (planche kpi-evolutions, 1e). Compact, pour une barre latérale ou un encart. per-row, cols, span et gap sont sans effet dans ce mode. Les KPI enfants perdent leur propre liseré (règle portée par dsfr-data-kpi, sur le sélecteur dsfr-data-kpi-group[orientation="vertical"]) — ils sont lus au rendu : un changement d'orientation APRÈS le montage ne les remet pas en forme. Le liseré du groupe est bleu info ; il ne suit pas le color-token des enfants.

Expressions de calcul

Grammaire commune du pipeline champ:fn (#303) — l'ancienne forme inversee fn:champ reste lue mais est depreciee.

Formats d'affichage

FormatExemple (valeur : 32073247.5)Description
nombre32 073 248Entier avec separateurs de milliers (arrondi)
euro32 073 248 €Montant en euros avec separateurs de milliers
pourcentage75,5 %Pourcentage (divise par 100, 1 decimale max)
decimal32 073 247,50Nombre decimal (1 a 2 decimales)
compact32,1 MNotation compacte fr-FR (v0.16.0) : 14 800 000 → « 14,8 M », 6 700 → « 6,7 k » (Intl compact, 1 décimale max)

Tous les formats utilisent la locale francaise (separateur de milliers = espace, decimales = virgule).

Exemples

1. KPI simple avec valeur directe

<dsfr-data-kpi
  source="meta"
  value="total"
  label="Sites suivis"
  icon="ri-global-line"
  format="nombre">
</dsfr-data-kpi>

2. KPI avec moyenne et seuils

<dsfr-data-kpi
  source="sites"
  value="score_rgaa:avg"
  label="Score RGAA moyen"
  format="pourcentage"
  threshold-green="80"
  threshold-orange="50">
</dsfr-data-kpi>

3. KPI avec comptage conditionnel

<dsfr-data-kpi
  source="sites"
  value="count:statut:actif"
  label="Sites actifs"
  icon="ri-checkbox-circle-line"
  color-token="vert">
</dsfr-data-kpi>

4. KPI avec min/max

<dsfr-data-kpi source="sites" value="score_rgaa:max" label="Meilleur score" format="pourcentage" color-token="vert"></dsfr-data-kpi>
<dsfr-data-kpi source="sites" value="score_rgaa:min" label="Plus bas score" format="pourcentage" color-token="rouge"></dsfr-data-kpi>

5. Carte baromètre — titre (heading) + ligne d'évolution (lines)

heading = surtitre au-dessus de la valeur ; lines = ligne secondaire data-driven (signe +, suffixe, couleur « auto » verte si hausse) ; label = légende en bas. Source mono-objet (un seul enregistrement courant) supportée.

<dsfr-data-source id="baro" data='[{"immat_ve":37849,"immat_ve_evol":92.5}]'></dsfr-data-source>

<dsfr-data-kpi source="baro"
  heading="Immat. VE — véhicules particuliers"
  value="immat_ve"
  format="nombre"
  lines='[{"value":"immat_ve_evol","sign":true,"suffix":"vs mai 2025","color":"auto"}]'
  label="Donnée mai 2026">
</dsfr-data-kpi>

6. Tableau de bord complet (CSS manuel)

Grouper des KPIs avec dsfr-data-kpi-group

7. Colonnes egales (cols="4")

<dsfr-data-kpi-group cols="4">
  <dsfr-data-kpi source="meta" value="total" label="Sites suivis"></dsfr-data-kpi>
  <dsfr-data-kpi source="sites" value="score_rgaa:avg" label="RGAA moyen"></dsfr-data-kpi>
  <dsfr-data-kpi source="sites" value="score_dsfr:avg" label="DSFR moyen"></dsfr-data-kpi>
  <dsfr-data-kpi source="sites" value="count:certificat_valide:false" label="Expires"></dsfr-data-kpi>
</dsfr-data-kpi-group>

8. Largeurs personnalisees (col)

<dsfr-data-kpi-group>
  <dsfr-data-kpi source="meta" value="total" label="Sites suivis" col="6"></dsfr-data-kpi>
  <dsfr-data-kpi source="sites" value="score_rgaa:avg" label="RGAA moyen" col="3"></dsfr-data-kpi>
  <dsfr-data-kpi source="sites" value="score_rgaa:max" label="Meilleur RGAA" col="3"></dsfr-data-kpi>
</dsfr-data-kpi-group>

9. Espacement large (gap="lg")

<dsfr-data-kpi-group cols="2" gap="lg">
  <dsfr-data-kpi source="sites" value="score_rgaa:min" label="Plus bas RGAA" color-token="rouge"></dsfr-data-kpi>
  <dsfr-data-kpi source="sites" value="score_rgaa:max" label="Meilleur RGAA" color-token="vert"></dsfr-data-kpi>
</dsfr-data-kpi-group>

Habillage : icône, pictogramme, image, liseré, teinte

Évolutions d'affichage de la planche « kpi-evolutions » (tours 1 à 3). Rien côté données, et rien sans les nouveaux attributs : sans eux, le rendu est celui des exemples 1 à 9, byte à byte (tests/kpi-rendu-retrocompat.test.ts). Les pictogrammes sont servis depuis specs/assets/pictograms/ — même origine que la page : un <use href> vers un CDN n'est pas rendu par les navigateurs.

10. Tour 1 — icône, pictogramme, image (1a à 1d)

1a et 1b : la planche montrait une icône de 48 et 64 px ; l'échelle d'icon-size s'arrête à md = 2 rem (la plus grande icône documentée du DSFR), donc ces deux vignettes se rendent avec un pictogramme. 1c : l'icône dans le libellé est le rendu par défaut, inchangé.

1a · picto="environment/leaf" picto-base="…" icon-position="top" icon-size="sm"

1b · picto="buildings/city-hall" icon-position="right"

1c · icon="ri-truck-line" (rendu par défaut)

1d · image="…" image-alt="" (bandeau 16:9)

<dsfr-data-kpi source="evo" heading="Immatriculations" value="immat" label="…"
  picto="environment/leaf" picto-base="../assets/pictograms/" icon-position="top" icon-size="sm"></dsfr-data-kpi>
<dsfr-data-kpi source="evo" heading="Part de marché" value="pdm" format="pourcentage" label="…"
  picto="buildings/city-hall" picto-base="../assets/pictograms/" icon-position="right"></dsfr-data-kpi>
<dsfr-data-kpi source="evo" heading="Poids lourds" value="pl" format="pourcentage" label="…"
  icon="fr-icon-truck-line" color-token="vert"></dsfr-data-kpi>
<dsfr-data-kpi source="evo" heading="Pompes à chaleur" value="pac" label="…"
  image="../assets/kpi-image-demo.svg" image-alt=""></dsfr-data-kpi>

11. Tour 1 — mode vertical (1e à 1g)

1e · <dsfr-data-kpi-group orientation="vertical">

1f · orientation="vertical" picto="environment/sun" (tuile centrée)

1g · orientation="vertical" (sans picto : aligné à gauche)

12. Tour 2 — image horizontale et formats de liseré (2a à 2i)

2a · image-position="left"

2b · image-position="right"

2c · border="left"

2d · border="top"

2e · border="bottom"

2f · border="outline"

2g · border="left-short"

2h · border="none" tint

2i · border="none"

13. Tour 3 — couleurs illustratives (3a à 3c)

Une couleur illustrative dit une catégorie (thème, ministère, famille de données) ; l'état reste aux quatre tokens sémantiques et aux seuils. Liseré et icône : var(--border-plain-<nom>) ; fond teinté : var(--background-contrast-<nom>). Aucun hexadécimal, le mode sombre suit. Sous tint, la valeur reste en gris titre.

3a · color-token="green-emeraude" icon="fr-icon-leaf-line" icon-position="top"

3b · color-token="green-emeraude" tint border="none"

3c · les 17 valeurs acceptées — liseré (teinte pleine) et fond (‑950)

<dsfr-data-kpi source="evo" heading="Environnement" value="env" format="pourcentage" label="…"
  color-token="green-emeraude" icon="fr-icon-leaf-line" icon-position="top"></dsfr-data-kpi>
<dsfr-data-kpi … color-token="green-emeraude" tint border="none"></dsfr-data-kpi>
<dsfr-data-kpi … color-token="brown-cafe-creme" tint="975"></dsfr-data-kpi>