-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Radio Items selection gets deselected on web page when converting html to pdf #944
Description
When rendering a pdf with html2pdf in a Plotly Dash application, the radio items selection gets unselected after html2pdf has finished rendering the pdf page. At app startup, the selection looks like:
After rendering the pdf the selection is gone:
This is purely a visual effect because the html and css still show that Option A is selected. If I inspect the value in the Dash framework, it also says Option A is still selected. I also opened a post on the Dash forum on this buggy behavior.
I tried with v0.10.1 and v0.14.0 of html2pdf and it occurs in both versions. On the Dash side, the behavior started since their most recent update, Dash 4, released in February 2026. That update did a major overhaul on the styling of many of their components. Since this version, the html definition for the RadioItems component changed a lot. Earlier versions of Dash did not have the buggy behavior with the selection dissappearing.
My attempt at figuring out where the root cause lies ended with the AI telling me that the name attribute, that is part of the new style, is the source of the bug, creating a conflict after the clone is finished. Unfortunately, I don't know enough of the html2pdf process to understand if this is true. I found 2 older issues reporting the same issue: #257 and #267, but the solutions there didn't help much for me.
What is causing this issue and what is the solution? I attached the html for the RadioItems component and a small Dash app below for reproducing the issue. If in the end the issue is more on the Dash side, I invite you to add to the discussion in the Plotly Dash forum post.
RadioItems html
Dash 4
<div id="radio-items" class="dash-options-list dash-radioitems " role="listbox">
<label class="dash-options-list-option selected" role="option" aria-selected="true">
<span class="dash-options-list-option-wrapper">
<input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="A" checked="">
</span>
<span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
<span>Option A</span>
</span>
</label>
<label class="dash-options-list-option" role="option" aria-selected="false">
<span class="dash-options-list-option-wrapper">
<input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="B">
</span>
<span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
<span>Option B</span>
</span>
</label>
<label class="dash-options-list-option" role="option" aria-selected="false">
<span class="dash-options-list-option-wrapper">
<input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="C">
</span>
<span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
<span>Option C</span>
</span>
</label>
</div>Dash 3 (and older)
<div id="radio-items">
<label style="display: block; margin-bottom: 8px;" class="">
<input class="" type="radio" checked="">Option A
</label>
<label style="display: block; margin-bottom: 8px;" class="">
<input class="" type="radio">Option B
</label>
<label style="display: block; margin-bottom: 8px;" class="">
<input class="" type="radio">Option C
</label>
</div>Dash App
requirements
Install the requirements
# Interaction bad :(
dash==4.0.0
plotly==6.6.0
dash-bootstrap-components==2.0.4
change Dash version to 3.3.0 to get good behavior
Sample app
Copy the code in a Python file and just run the file to run the app. Click on the localhost link that the terminal shows to open the app in your browser.
This version of the app loads html2pdf v0.10.1 on startup as a script. If you want to try version 0.14.0 do the following:
- Comment or delete the whole
app.index_stringvariable - Create an assets directory next to the Python file
- Put the html2pdf (bundle) file with the desired version in the assets folder
- Close the open browser tab and rerun the Dash app.
The folder structure then looks like
parent_dir
├── assets
| └──html2pdf.bundle.min.js
└── app.py
import dash
from dash import dcc, html, clientside_callback, Input, Output, State, callback
import dash_bootstrap_components as dbc
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.index_string = """
<!DOCTYPE html>
<html>
<head>
{%metas%}
<title>{%title%}</title>
{%favicon%}
{%css%}
</head>
<body>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"
integrity="sha512-GsLlZN/3F2ErC5ifS5QtgpiJtWd43JWSuIgh7mbzZ8zBps+dvLusV+eNQATqgA/HdeKFVgA5v3S/cIrLF7QnIg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
{%renderer%}
</footer>
</body>
</html>
"""
button_pdf_report = dbc.Button(
id="button_download_report",
children="Download PDF report",
color="primary",
outline=True,
style={"marginLeft": "10px", "width": "200px"},
)
app.layout = dbc.Container(
[
html.H2("Radio Items & PDF Report", className="my-4"),
dcc.RadioItems(
id="radio-items",
options=[
{"label": "Option A", "value": "A"},
{"label": "Option B", "value": "B"},
{"label": "Option C", "value": "C"},
],
value="A",
labelStyle={"display": "block", "marginBottom": "8px"},
),
html.Div(
[
dbc.Button(
id="button_show_value",
children="Show Selected Value",
color="secondary",
outline=True,
style={"width": "200px"},
),
html.Div(id="selected-value-output", className="mt-2"),
],
className="mt-4",
),
html.Div(button_pdf_report, className="mt-4"),
],
className="p-4",
id="main_container",
style={"minWidth": "1253px"}
)
clientside_callback(
"""
function (button_clicked) {
if (button_clicked && button_clicked > 0) {
var mainContainerElement = document.getElementById("main_container");
var main_container_width = parseInt(mainContainerElement.style.minWidth);
var opt = {
// margin units are those that are defined in jsPDF key below.
margin: 10,
filename: "test_file.pdf",
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 3,
width: main_container_width,
dpi: 300,
},
jsPDF: { unit: 'mm', format: 'A4', orientation: 'p' },
};
html2pdf().from(mainContainerElement).set(opt).save().then(function() {
// Force a repaint of the radio items after pdf generation
var radioEl = document.getElementById("radio-items");
if (radioEl) {
radioEl.style.display = 'none';
// Reading offsetHeight forces the browser to flush layout
void radioEl.offsetHeight;
radioEl.style.display = '';
}
});
}
}
""",
Input(component_id="button_download_report", component_property="n_clicks"),
)
@callback(
Output("selected-value-output", "children"),
Input("button_show_value", "n_clicks"),
State("radio-items", "value"),
prevent_initial_call=True,
)
def show_selected_value(n_clicks, value):
return f"Selected value: {value}"
if __name__ == "__main__":
app.run(debug=True)