diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index 137f2e5c12211..24def10266ab7 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -523,6 +523,7 @@ I/O
- Bug in :func:`read_html`, tail texts were removed together with elements containing ``display:none`` style (:issue:`51629`)
- Bug in :func:`read_sql` when reading multiple timezone aware columns with the same column name (:issue:`44421`)
- Bug in :func:`read_xml` stripping whitespace in string data (:issue:`53811`)
+- Bug in :meth:`DataFrame.to_html` where ``colspace`` was incorrectly applied in case of multi index columns (:issue:`53885`)
- Bug when writing and reading empty Stata dta files where dtype information was lost (:issue:`46240`)
- Bug where ``bz2`` was treated as a hard requirement (:issue:`53857`)
diff --git a/pandas/io/formats/html.py b/pandas/io/formats/html.py
index 32a0cab1fbc41..151bde4e1c4c2 100644
--- a/pandas/io/formats/html.py
+++ b/pandas/io/formats/html.py
@@ -73,10 +73,16 @@ def __init__(
self.table_id = table_id
self.render_links = render_links
- self.col_space = {
- column: f"{value}px" if isinstance(value, int) else value
- for column, value in self.fmt.col_space.items()
- }
+ self.col_space = {}
+ is_multi_index = isinstance(self.columns, MultiIndex)
+ for column, value in self.fmt.col_space.items():
+ col_space_value = f"{value}px" if isinstance(value, int) else value
+ self.col_space[column] = col_space_value
+ # GH 53885: Handling case where column is index
+ # Flatten the data in the multi index and add in the map
+ if is_multi_index and isinstance(column, tuple):
+ for column_index in column:
+ self.col_space[str(column_index)] = col_space_value
def to_string(self) -> str:
lines = self.render()
diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py
index 9c128756339ab..bea0eebef2cf6 100644
--- a/pandas/tests/io/formats/test_to_html.py
+++ b/pandas/tests/io/formats/test_to_html.py
@@ -896,3 +896,59 @@ def test_to_html_float_format_object_col(datapath):
result = df.to_html(float_format=lambda x: f"{x:,.0f}")
expected = expected_html(datapath, "gh40024_expected_output")
assert result == expected
+
+
+def test_to_html_multiindex_col_with_colspace():
+ # GH#53885
+ df = DataFrame([[1, 2]])
+ df.columns = MultiIndex.from_tuples([(1, 1), (2, 1)])
+ result = df.to_html(col_space=100)
+ expected = (
+ '
\n'
+ " \n"
+ " \n"
+ ' | \n'
+ ' 1 | \n'
+ ' 2 | \n'
+ "
\n"
+ " \n"
+ ' | \n'
+ ' 1 | \n'
+ ' 1 | \n'
+ "
\n"
+ " \n"
+ " \n"
+ " \n"
+ " 0 | \n"
+ " 1 | \n"
+ " 2 | \n"
+ "
\n"
+ " \n"
+ "
"
+ )
+ assert result == expected
+
+
+def test_to_html_tuple_col_with_colspace():
+ # GH#53885
+ df = DataFrame({("a", "b"): [1], "b": [2]})
+ result = df.to_html(col_space=100)
+ expected = (
+ '\n'
+ " \n"
+ ' \n'
+ ' | \n'
+ ' (a, b) | \n'
+ ' b | \n'
+ "
\n"
+ " \n"
+ " \n"
+ " \n"
+ " 0 | \n"
+ " 1 | \n"
+ " 2 | \n"
+ "
\n"
+ " \n"
+ "
"
+ )
+ assert result == expected