Skip to content

Radio Items selection gets deselected on web page when converting html to pdf #944

@aGitForEveryone

Description

@aGitForEveryone

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:

Image

After rendering the pdf the selection is gone:

Image

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:

  1. Comment or delete the whole app.index_string variable
  2. Create an assets directory next to the Python file
  3. Put the html2pdf (bundle) file with the desired version in the assets folder
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions