Skip to content

basic theme stretches images that have both width and height attributes #14799

Open

Description

@drbenvincent

Describe the bug

Images that carry both width and height attributes are stretched whenever the column is narrower than the width attribute. sphinx/themes/basic/static/basic.css.jinja (currently the img rule at line 103) sets max-width: 100% and does not set height: auto, so the used width shrinks while the height attribute stays fixed. The default object-fit: fill then distorts the bitmap.

Expected: the image scales down uniformly, keeping the ratio implied by its attributes.

This is the incomplete follow-up to #1093. That issue added max-width: 100% so images would fit a narrow window, which was the right idea, but without height: auto the height attribute is left behind. The rule is unchanged on master. Themes that inherit basic and do not override img inherit the bug. pydata-sphinx-theme 0.21.0 is one of those: inherit = basic, and its own stylesheet only applies height: auto to .img-fluid.

This is not the first time it has shown up, which is why a regression test seems worth adding with the fix. The Read the Docs theme has carried the missing declaration for years (src/sass/_theme_rst.sass: "Sphinx by default applies HxW style attributes to images. This fixes that oversight."). Downstream, pymc-labs/pymc-forecast#66 hit the same vertical stretch again this month and patched it in project CSS. We are seeing it once more on the CausalPy docs (pymc-labs/CausalPy#1357).

How to Reproduce

$ mkdir repro && cd repro
$ cat > conf.py << 'EOF'
project = "repro"
html_theme = "basic"
EOF
$ cat > index.rst << 'EOF'
.. image:: plot.png
   :width: 800px
   :height: 400px
EOF
$ # any local PNG is fine; the attributes are what matter
$ python -c "import struct,zlib,pathlib; w,h=8,4; raw=b''.join(b'\x00'+b'\xff\x00\x00'*w for _ in range(h)); png=b'\x89PNG\r\n\x1a\n'+b''.join(struct.pack('>I',len(c))+c+struct.pack('>I',zlib.crc32(c)&0xffffffff) for c in [b'IHDR'+struct.pack('>IIBBBBB',w,h,8,2,0,0,0), b'IDAT'+zlib.compress(raw), b'IEND']); pathlib.Path('plot.png').write_bytes(png)"
$ sphinx-build -b html . _build
$ # open _build/html/index.html in a window narrower than 800px

The built page contains <img width="800" height="400" ...>. In a 400px-wide column the image renders at 400×400 instead of 400×200.

The same thing happens for notebook output, which is how we noticed it. MyST-NB emits both attributes at the 2× display size (a 2773×1577 PNG becomes <img width="1386" height="788">, ratio 1.76). On https://causalpy.readthedocs.io/en/latest/notebooks/interrupted-time-series-lift-test.html, Chrome computed style at a 390px viewport is max-width: 100%, width: 358px, height: 788px, object-fit: fill. Displayed ratio 0.45. At 1440px with the sidebars open the same figure is 720×788 instead of 720×410. Images whose width attribute is already below the column width are fine, so it is easy to miss on a wide monitor.

Injecting img { height: auto; } restores 358×204 (ratio 1.758). Navbar logos that set height with a more specific selector, such as .navbar-brand img { height: 100% }, are unaffected.

Environment Information

Platform:              darwin; (macOS-27.2-arm64-arm-64bit)
Python version:        3.12.12 (main, Dec 17 2025, 21:07:08) [Clang 21.1.4 ])
Python implementation: CPython
Sphinx version:        9.1.0
Docutils version:      0.22.4
Jinja2 version:        3.1.6
Pygments version:      2.21.0

Reproduced in Chrome against docs built with Sphinx 9.1.0 and pydata-sphinx-theme 0.21.0, which loads _static/basic.css through theme.css (@import url("../basic.css")). The same img rule is in basic.css.jinja on master.

Sphinx extensions

[] for the minimal reproduction above. The live page also uses myst_nb, which is what emits both attributes for notebook figures. The distortion does not depend on it: any image with both HTML dimension attributes is enough.

Additional context

Suggested fix in sphinx/themes/basic/static/basic.css.jinja:

img {
    border: 0;
    max-width: 100%;
    height: auto;
}

height: auto still uses the width and height attributes for the intrinsic ratio, including before the image loads, so this should not bring layout shift back. It is the same declaration the Read the Docs theme has shipped for years.

There does not seem to be a browser harness in tests/, so a rendered-ratio check is probably more than this suite wants. A stylesheet assertion fits tests/test_theming and would have failed on current master. Something in this shape:

import re

@pytest.mark.sphinx("html", testroot="basic", confoverrides={"html_theme": "basic"})
def test_basic_css_images_keep_aspect_ratio(app):
    app.build()
    css = (app.outdir / "_static" / "basic.css").read_text(encoding="utf-8")
    rule = re.search(r"(?m)^img \{\n(?:    .+\n)*\}", css)
    assert rule is not None, "basic.css has no top-level img rule"
    assert "height: auto" in rule.group(0)

That locks the #1093 follow-up. A child theme can still override it, but the inherited default would be correct, which is the case that keeps reaching downstream docs.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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