Aller au contenu

Badges

Status: Implemented Last reviewed: 2026-05-06 Sources of truth: static/css/base.css — base badge style + semantic palette (badge-status-*, badge-type) + cap-table extended palette (badge-entity-* snake_case + badge-movement-* with per-variant tokens). Module-local: static/workplan/action_list.cssbadge-workplan-status-*. Examples: apps/entities/templates/entities/entity_detail.html, templates/skeletons/skeleton_detail.html.

Scope

Inline status / type indicators rendered as <span class="badge ...">. Covers: which class to use for which semantic meaning, the family-modifier pattern (entity types, movement types), and the rule against raw Bootstrap utility colors.

Out of scope: alerts (.alert-*), action buttons (.btn-action-*). Table-row accents (data-movement-group left borders) live in register.css and consume the --movement-accent-* tokens defined in base.css alongside their matching badge classes.

Hard rules

  1. Never use raw Bootstrap bg-* / text-bg-* on a badge. No bg-success, bg-warning, bg-primary, bg-secondary, etc. Always pick a semantic class from the catalogue below.
  2. Always combine badge + one semantic class. <span class="badge badge-status-active">badge alone gives shape; the semantic class gives meaning + colour.
  3. Badge text is translatable. Wrap the label in {% trans %} (templates) or _() (Python — usually via get_FOO_display() from a model choices).
  4. Badges carry meaning, not decoration. If a badge isn't communicating state, type, or category — it shouldn't be a badge. Use a chip, tag, or plain text.

The catalogue

Status badges (4 states)

For row-level / record-level lifecycle state. Pick exactly one per badge.

Class Meaning Color Use for
badge-status-active "this is on / live / current" green tint (--success-100/700) Active, Oui, Enabled, Open, Confirmed
badge-status-pending "in transition / awaiting action" amber tint (--warning-100/700) Pending, En attente, Draft, Provisoire
badge-status-inactive "off / closed / archived" red tint (--error-100/700) Inactive, Non, Closed, Archived, Disabled
badge-status-neutral "informational, no state judgement" grey (--bg-tertiary/--text-secondary) Country tags, version numbers, period labels, anything that isn't a lifecycle

Don't invent a 5th status. If you find yourself wanting "warning" + "pending" + "info" as separate states, you're conflating lifecycle with priority. Use status for lifecycle; use a separate column / chip for priority.

Type / category badges

For "what kind of thing is this." Always use badge-type as the base; optionally add a family modifier alongside.

<span class="badge badge-type">{% trans "Catégorie" %}</span>
<span class="badge badge-type badge-entity-holding">{{ entity.get_entity_type_display }}</span>

Default badge-type = brand tint (--bg-interactive / --brand-700). Family modifiers override the colour while keeping the shape.

Family modifier — entity types

All defined in static/css/base.css. Worn alongside badge-type.

Compact set (general use, in base.css):

Class Tint
badge-entity-holding brand-100 / brand-800
badge-entity-spfpl info-100 / info-800
badge-entity-operating success-100 / success-800
badge-entity-service neutral-200 / neutral-700
badge-entity-other neutral-100 / neutral-600

Extended set (cap-table specifics, in base.css):

badge-entity-top_holding, badge-entity-intermediate_holding, badge-entity-central_holding, badge-entity-spfpl_holding, badge-entity-operating_practice, badge-entity-service_company.

The extended set uses full snake-cased names matching the model's entity_type choices; the compact set uses short keys. They're not aliases — pick the one that matches the value coming from your data.

The model exposes a helper for picking the right modifier: entity.entity_type_badge_class returns the CSS class string. Render as <span class="badge badge-type {{ entity.entity_type_badge_class }}">.

Family modifier — pipeline stage (CRM funnel position)

For "where in the acquisition funnel is this prospect." Worn alongside .badge. The 12 CRM stages are mapped along the brand palette from --brand-50 (lightest, top of funnel) to --brand-900 (darkest, closed) so a row's stage is readable both as label and as a colour-intensity gradient. Two adjacent stages may share a shade where a finer split would reduce contrast.

Class Stage Tint
badge-stage-strategic_target Cible stratégique brand-50 / brand-700
badge-stage-active_prospect Prospect actif brand-100 / brand-800
badge-stage-engaged Engagé brand-200 / brand-800
badge-stage-qualified Qualifié brand-200 / brand-900
badge-stage-data_received Données reçues brand-300 / brand-900
badge-stage-assessment_confirmed Évaluation confirmée brand-400 / brand-50
badge-stage-loi_sent LOI envoyée brand-500 / brand-50
badge-stage-loi_signed LOI signée brand-600 / brand-50
badge-stage-dd_done DD terminée brand-700 / brand-50
badge-stage-doc_drafting Rédaction docs brand-700 / brand-50
badge-stage-doc_signed Docs signés brand-800 / brand-50
badge-stage-acquired Acquis brand-900 / brand-50
<span class="badge badge-stage-{{ prospect.stage }}">{{ prospect.get_stage_display }}</span>

This is CRM-specific; do not reuse for non-funnel taxonomies. For generic "what kind of thing" tagging, prefer badge-type + an entity family modifier.

Family modifier — temperature (heat indicator)

For "how hot is this lead." Worn alongside .badge. Pairs a small leading colored dot with a light-tinted background — same colour family as the pipeline's per-card left-border accent.

Class Meaning Tint Dot
badge-temperature-hot Chaud / hot prospect error-50 / error-700 error-500
badge-temperature-warm Tiède / warm prospect warning-50 / warning-700 warning-500
badge-temperature-cold Froid / cold prospect info-50 / info-700 info-500
badge-temperature-none No temperature set bg-tertiary / text-secondary text-secondary
<span class="badge badge-temperature-hot">{% trans "Chaud" %}</span>

The dot is rendered via a ::before pseudo-element — no extra markup. The shared crm/partials/temperature_badge.html partial picks the right modifier from prospect.temperature.

Family modifier — workplan status

For the workplan action lifecycle (5 states). Module-local — defined in static/workplan/action_list.css (workplan-only, not shared). Same pattern as badge-temperature-*: light tinted background, saturated text, leading dot via ::before. The action-row inline-edit trigger is a <button> carrying these classes plus .workplan-status-trigger to strip the button chrome.

Class Meaning Tint Dot
badge-workplan-status-not_started À faire bg-tertiary / text-secondary text-secondary
badge-workplan-status-in_progress En cours info-50 / info-700 info-500
badge-workplan-status-blocked Bloqué error-50 / error-700 error-500
badge-workplan-status-done Terminé success-50 / success-700 success-500
badge-workplan-status-cancelled Annulé neutral-50 / neutral-500 neutral-500
<span class="badge badge-workplan-status-{{ action.status }}">{{ action.get_status_display }}</span>

The 5-state palette is workplan-specific and intentionally separate from the 4-state badge-status-* family — workplan needs to distinguish "in progress" (blue) from "done" (green) on the same row, which the lifecycle palette can't express. Don't reuse this family outside workplan.

Family modifier — movement types

Defined in static/css/base.css. Worn alongside badge-type. Used on the share-register movement list. Each variant has a matching --movement-accent-* token used as the row left-border in register.css.

badge-movement-creation · badge-movement-transfer · badge-movement-modification · badge-movement-conversion · badge-movement-transformation · badge-movement-other.

Each has a colour-coded pair plus a left-border accent on the parent <tr> (via data-movement-group="...") for faster visual scanning of the register table.

Common patterns

Boolean field on a list / detail page

{% if obj.is_active %}
    <span class="badge badge-status-active">{% trans "Oui" %}</span>
{% else %}
    <span class="badge badge-status-inactive">{% trans "Non" %}</span>
{% endif %}

Lifecycle status with multiple states (4-way)

{% if entity.lifecycle_status == 'active' %}
    <span class="badge badge-status-active">{{ entity.get_lifecycle_status_display }}</span>
{% elif entity.lifecycle_status == 'in_creation' %}
    <span class="badge badge-status-pending">{{ entity.get_lifecycle_status_display }}</span>
{% elif entity.lifecycle_status == 'closed' %}
    <span class="badge badge-status-inactive">{{ entity.get_lifecycle_status_display }}</span>
{% else %}
    <span class="badge badge-status-neutral">{{ entity.get_lifecycle_status_display }}</span>
{% endif %}

Type with family modifier

<span class="badge badge-type {{ entity.entity_type_badge_class }}">
    {{ entity.get_entity_type_display }}
</span>

Status badge inside a detail-page heading

Wrap the status badge in <div class="mt-2"> immediately under the title — see templates/skeletons/skeleton_detail.html and guidelines/ui/detail-pages.md (when written).

Adding a new family modifier

If you need a new badge family (e.g. for a new domain like contracts, payments):

  1. Decide if it really needs its own family. A new colour for an existing family is rarely justified — try a status badge first.
  2. Add the CSS to static/css/base.css if it's a shared family (used by 2+ apps). Add to a per-module CSS file (e.g. register.css) if it's truly module-local.
  3. Use design tokens (var(--*)) only — do not write literal oklch(...) or #hex values even if existing entries do (they're known deviations).
  4. Add a row to the catalogue in this file.
  5. If the variant is data-driven, expose it via a model property (mirror entity_type_badge_class) so templates don't carry the mapping logic.
  6. Bump Last reviewed at the top.

Anti-patterns

  • <span class="badge bg-success"> — use badge-status-active.
  • <span class="badge text-bg-light"> — use badge-status-neutral.
  • <span class="badge badge-pill bg-warning text-dark"> — use badge-status-pending.
  • ❌ Raw colours via style="background: #..." — same hard rule as design-tokens.
  • ❌ Hardcoded English / French strings inside <span class="badge"> — wrap in {% trans %} or use get_FOO_display().
  • ❌ Inventing a 5th status (badge-status-warning, badge-status-info). Stretch the 4 existing ones or use badge-type with a modifier.
  • ❌ Using badge-type for a lifecycle state (or badge-status-* for a category). The family carries semantics — don't cross-wire them.

Known deviations

Surfaced by the 2026-04 audit (see roadmap/backlog/ui-design-system-mechanical-fixes-2026-04.md):

  • apps/annuaire/templates/annuaire/changes.html:125,130,135 — three bg-secondary badges. Replace with badge-status-neutral.
  • apps/finance/templates/finance/supplier_overview.html:138bg-warning text-dark. Replace with badge-status-pending.
  • apps/collection/templates/collection/agent_detail.html:163text-bg-light. Replace with badge-status-neutral.

Internal inconsistencies (not audit findings — improvement candidates):

  • Two naming schemes for entity-type variants. base.css defines both a compact set (badge-entity-holding, -spfpl, -operating, -service, -other — 5 classes using brand/info/success/neutral palette) and a snake_case set (badge-entity-top_holding, -intermediate_holding, -central_holding, -spfpl_holding, -operating_practice, -service_company — 6 classes with per-variant unique hue tokens). They're not aliases. Picking the right one currently requires knowing where the variant is consumed. Tracked as roadmap/ideas/ui-badge-entity-naming-convergence.md — separate refactor, not blocking.

When to break the rules

  • Charts and data visualisations (e.g. ApexCharts, kanban headers) often need bespoke colour badges that don't fit the 4 statuses or the type families. Use chart palette tokens (--chart-color-1, …, --chart-color-6) not raw hex, but skip the badge-* semantic classes.
  • Third-party widget integration (e.g. an embedded Doctolib widget) may emit its own badges. Don't try to override; isolate in a wrapper.