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.css—badge-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¶
- Never use raw Bootstrap
bg-*/text-bg-*on a badge. Nobg-success,bg-warning,bg-primary,bg-secondary, etc. Always pick a semantic class from the catalogue below. - Always combine
badge+ one semantic class.<span class="badge badge-status-active">—badgealone gives shape; the semantic class gives meaning + colour. - Badge text is translatable. Wrap the label in
{% trans %}(templates) or_()(Python — usually viaget_FOO_display()from a modelchoices). - 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 |
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 |
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):
- Decide if it really needs its own family. A new colour for an existing family is rarely justified — try a status badge first.
- Add the CSS to
static/css/base.cssif 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. - Use design tokens (
var(--*)) only — do not write literaloklch(...)or#hexvalues even if existing entries do (they're known deviations). - Add a row to the catalogue in this file.
- 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. - Bump
Last reviewedat the top.
Anti-patterns¶
- ❌
<span class="badge bg-success">— usebadge-status-active. - ❌
<span class="badge text-bg-light">— usebadge-status-neutral. - ❌
<span class="badge badge-pill bg-warning text-dark">— usebadge-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 useget_FOO_display(). - ❌ Inventing a 5th status (
badge-status-warning,badge-status-info). Stretch the 4 existing ones or usebadge-typewith a modifier. - ❌ Using
badge-typefor a lifecycle state (orbadge-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— threebg-secondarybadges. Replace withbadge-status-neutral.apps/finance/templates/finance/supplier_overview.html:138—bg-warning text-dark. Replace withbadge-status-pending.apps/collection/templates/collection/agent_detail.html:163—text-bg-light. Replace withbadge-status-neutral.
Internal inconsistencies (not audit findings — improvement candidates):
- Two naming schemes for entity-type variants.
base.cssdefines 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 asroadmap/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 thebadge-*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.