Skip to content

Feature: pluggable HTML themes via Jinja2 template inheritance #1029

@RonnyPfannschmidt

Description

@RonnyPfannschmidt

Motivation

There is clear user demand for a modernised report UI (see #913, #904, #884). Currently, anyone who wants a different visual style has to override CSS after the fact or maintain a separate fork. pytest-html could instead offer first-class theme support, making it trivial to ship alternative report styles and for the community to contribute new ones.

Proposed design

1. Jinja2 template inheritance with {% extends %}

Factor the current index.jinja2 into a base template and per-theme overrides:

resources/
  base.jinja2              # shared HTML skeleton with {% block %} placeholders
  classic/
    layout.jinja2          # {% extends "base.jinja2" %} — current look
    style.css
  modern/
    layout.jinja2          # {% extends "base.jinja2" %} — card-based dashboard
    style.css

Only 4 blocks actually differ between visual styles:

  • {% block header %} — page title / navbar
  • {% block summary %} — run stats presentation (flat text vs stat cards with percentages)
  • {% block filters %} — checkboxes vs toggle switches
  • {% block footer %} — generation info placement

Everything else is shared in base.jinja2: the results table, the <template> for result rows, the media viewer, the JS data container, and the script include.

2. Config option

# pytest.ini / pyproject.toml
[tool.pytest.ini_options]
html_theme = "modern"   # default: "classic"
# plugin.py
parser.addini(
    "html_theme",
    type="string",
    default="classic",
    help="HTML report theme ('classic' or 'modern').",
)

3. Template loader change

Pass [theme_path, resources_path] to Jinja2's FileSystemLoader so each theme's layout.jinja2 can do {% extends "base.jinja2" %} and the loader resolves base.jinja2 from the parent resources directory. Minimal change to _read_template.

4. Extra template variables

A modern card-based theme benefits from a few additional pre-computed template variables. These are cheap to compute and can be passed unconditionally so all themes have them available:

  • start_time / end_time — formatted timestamps for the suite
  • duration_display — human-readable total duration
  • total_count — total tests excluding reruns

This is ~15 lines in _generate_report().

Benefits

  • Backward compatibleclassic is the default; no one's custom CSS breaks
  • Extensible — new themes are just a directory with layout.jinja2 + style.css
  • Low risk — the JS logic, data model, hooks, and Python backend are untouched
  • Community-friendly — lowers the bar for contributing visual improvements

What this does NOT change

  • No new features or hooks — purely a presentation-layer refactor
  • No change to the default appearance (classic remains default)
  • No JS modifications required

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions