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 compatible —
classic 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
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.jinja2into a base template and per-theme overrides: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 placementEverything 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
3. Template loader change
Pass
[theme_path, resources_path]to Jinja2'sFileSystemLoaderso each theme'slayout.jinja2can do{% extends "base.jinja2" %}and the loader resolvesbase.jinja2from 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 suiteduration_display— human-readable total durationtotal_count— total tests excluding rerunsThis is ~15 lines in
_generate_report().Benefits
classicis the default; no one's custom CSS breakslayout.jinja2+style.cssWhat this does NOT change