Skip to content
AscendQ.ca — Websites, Apps & SEO Systems

Performance 7 min read

How to Fix "Image elements do not have explicit width and height"

Image elements do not have explicit width and height
Set an explicit width and height on image elements to reduce layout shifts and improve CLS.

<img class="home_tabs_main_image" src="/hubfs/hr-solutions.webp" alt="HR Software and Solutions" loading="lazy">

When the browser parses your HTML, it lays out the page immediately — usually before a single image has downloaded. If an <img> tag carries width and height attributes, the browser computes the aspect ratio from them and reserves a correctly-shaped box in the layout, then paints the image into that box whenever it arrives. If the attributes are missing, the browser reserves NOTHING: the image occupies zero height until it downloads, and the moment it does, everything below it gets shoved down. Multiply by every image on the page and you get the jumpy, twitchy load that Google measures as Cumulative Layout Shift.

This matters beyond aesthetics. CLS is one of the three Core Web Vitals, and Core Web Vitals are a Google ranking signal measured from your real visitors — not just a lab score. Layout shift is also a genuine usability failure: it causes mis-clicks (the classic "the ad loaded and I tapped the wrong thing"), it disorients screen-magnifier users, and on long pages it makes reading feel broken. The fix costs two attributes per image, works WITH responsive layouts rather than against them, and modern browsers were specifically updated (back in 2019) to make this the standard mechanism for jank-free image loading.

You're In The Right Place If…

  • Lighthouse Performance diagnostics list "Image elements do not have explicit width and height" with a stack of offending <img> tags
  • Your Cumulative Layout Shift (CLS) score is above 0.1 — or PageSpeed Insights shows real-user CLS failing Core Web Vitals
  • Content visibly jumps down as images load — especially on slow connections or long pages with lazy-loaded images
  • Users mis-click things because a button moved at the exact moment an image above it finished loading
  • Your CMS module code SETS a size-attributes variable (e.g. HubSpot's sizeAttrs boilerplate) but the <img> tag never outputs it — the attributes exist in the template and still never reach the HTML

What The Attributes Actually Do (it Is Not What Most People Think)

The old-school belief is that width="558" height="393" forces the image to display at 558x393 pixels. Not anymore. In every modern browser, when your CSS also sizes the image (max-width: 100%, width: 100%, etc.), the HTML attributes are used for exactly one thing: computing the intrinsic ASPECT RATIO (558:393) so the browser can reserve a correctly-proportioned box before the file downloads. CSS still wins on final display size.

That is why the fix is safe on responsive sites: the attributes stop the layout jump, and your stylesheet keeps controlling how big the image actually renders at every breakpoint. The one companion rule your CSS needs — and most frameworks already ship it — is height: auto whenever you set a width in CSS. Without it, a CSS width plus an HTML height attribute can stretch the image out of proportion:

/* the standard companion rule — most resets/frameworks already include it: */
img {
  max-width: 100%;
  height: auto;
}

Every Reason To Add Them (not Just Lighthouse)

  • CLS / Core Web Vitals: reserved space = no layout jump = lower CLS, and CLS is a Google ranking signal measured from real Chrome users, not just lab runs
  • Mis-click prevention: shifting layouts cause users to click the wrong link or button at the exact moment content moves — a real conversion killer on CTAs near images
  • Lazy loading works properly: loading="lazy" images below the fold get correct scroll positions only if their space is reserved — otherwise the page length keeps changing as you scroll and anchors land in the wrong place
  • Faster perceived rendering: the browser can finalize layout in one pass instead of recalculating (reflowing) after every image arrives — cheaper on low-end phones
  • Stable anchors and back-button positions: jumping content breaks #section links and scroll restoration
  • Screen magnifier and low-vision users: content that relocates mid-read is far more disorienting at 400% zoom

How To Add Them, Case By Case

The values should be the image FILE's intrinsic pixel dimensions — or any pair with the same aspect ratio (the ratio is what the browser uses). Where to find them: hover the image URL in DevTools (shows "intrinsic size"), or open the file directly in a tab and read the title bar, or check the CMS media library details.

Plain HTML — add both attributes, no units:

<!-- BEFORE (reserves no space, causes shift): -->
<img class="home_tabs_main_image" src="/hubfs/hr-solutions.webp"
     alt="HR Software and Solutions" loading="lazy">

<!-- AFTER (browser reserves a 558:393 box immediately): -->
<img class="home_tabs_main_image" src="/hubfs/hr-solutions.webp"
     width="558" height="393"
     alt="HR Software and Solutions" loading="lazy">

<!-- Works identically WITH responsive srcset images —
     width/height describe the ratio, sizes/srcset pick the file: -->
<img src="/hs-fs/hubfs/hr-solutions.webp?width=558&name=hr-solutions.webp"
     srcset="/hs-fs/hubfs/hr-solutions.webp?width=332&name=hr-solutions.webp 332w,
             /hs-fs/hubfs/hr-solutions.webp?width=558&name=hr-solutions.webp 558w"
     sizes="(max-width: 991px) 100vw, 332px"
     width="558" height="393"
     alt="HR Software and Solutions" loading="lazy">

CMS Templates: Pull The Dimensions From The Image Field

Hard-coding numbers is fine for fixed images, but in CMS modules where editors swap images, read the dimensions from the platform. HubSpot image fields expose .width and .height; WordPress adds the attributes automatically for library images inserted the normal way (it only misses hand-written theme <img> tags); Shopify Liquid exposes image.width / image.height. The pattern everywhere is the same:

<!-- HubSpot (HubL) — dimensions travel with whatever image the editor picks: -->
<img src="{{ module.tab_image.src|escape_url }}"
     width="{{ module.tab_image.width }}" height="{{ module.tab_image.height }}"
     alt="{{ module.tab_image.alt|escape_attr }}" loading="lazy">

<!-- Shopify (Liquid): -->
<img src="{{ product.featured_image | image_url: width: 600 }}"
     width="600" height="{{ 600 | divided_by: product.featured_image.aspect_ratio | round }}"
     alt="{{ product.featured_image.alt | escape }}" loading="lazy">

The "ghost Variable" Bug: Your Module SETS The Attributes But Never OUTPUTS Them

Here is a failure mode we keep finding in real CMS themes, and it is sneaky because the code LOOKS correct. Module boilerplate (HubSpot's generated modules are a prime example) carefully builds a sizeAttrs variable with the right width/height for every size mode — and then the <img> tag below forgets to print it. The variable is set, never used: a ghost. Every image the module renders ships without dimensions, no matter how diligently editors fill in the image fields.

This is also why "just fix the images in the editor" does not work: many images never pass through the rich-text editor at all. Logo sliders, testimonial avatars, tab panels, card grids — those <img> tags live inside module templates, and if the template drops the attributes, every instance on every page inherits the bug. One template fix repairs them all; no amount of editor-side effort can.

The tell-tale pattern (HubSpot example) — sizeAttrs is built in three flavors, then the img tag only prints loadingAttr:

{# The GHOST — built with care, then never printed: #}
{% set sizeAttrs = 'width="{{ item.width|escape_attr }}" height="{{ item.height|escape_attr }}"' %}
{% if item.size_type == 'auto' %}
{% set sizeAttrs = 'width="{{ item.width|escape_attr }}" height="{{ item.height|escape_attr }}" style="max-width: 100%; height: auto;"' %}
{% endif %}
{% set loadingAttr = item.loading != 'disabled' ? 'loading="{{ item.loading|escape_attr }}"' : '' %}

<!-- BUG: sizeAttrs is missing from the tag -->
<img class="slider_item_image" src="{{ item.src|escape_url }}" alt="{{ item.alt|escape_attr }}" {{ loadingAttr }}>

<!-- FIX: print it -->
<img class="slider_item_image" src="{{ item.src|escape_url }}" alt="{{ item.alt|escape_attr }}" {{ sizeAttrs }} {{ loadingAttr }}>

{# Sweep the whole theme: search your modules for "sizeAttrs" (or width=) and check
   each <img> actually outputs it — the ghost is usually copy-pasted everywhere. #}

Find Every Offender In One Paste

Run this in the DevTools console on any page — it lists every image missing either attribute, with its file and intrinsic dimensions so you can copy the numbers straight in:

// List images missing width/height attributes + the values to add:
document.querySelectorAll('img').forEach(function (img) {
  if (!img.getAttribute('width') || !img.getAttribute('height')) {
    console.log(
      (img.currentSrc || img.src).split('?')[0],
      '→ add: width="' + img.naturalWidth + '" height="' + img.naturalHeight + '"'
    );
  }
});

Edge Cases Worth Knowing

  • SVGs: give them width/height too (or ensure the SVG file has a viewBox) — an SVG without either can collapse to 0 or default to 300x150 and shift the layout like any raster image
  • CSS background-images never shift (the element sizes itself), but they also never lazy-load natively — do not convert <img> to backgrounds just to dodge this audit
  • The <picture> element: put width/height on the inner <img> tag; if art-directed sources have DIFFERENT aspect ratios, modern browsers accept width/height on each <source> as well
  • Aspect-ratio CSS is an alternative (aspect-ratio: 558 / 393 on the img or a wrapper) — useful when HTML attributes are out of reach, but the attributes remain the standard, works-everywhere mechanism
  • Wrong values are worse than none if wildly off: the browser reserves the wrong shape, then STILL shifts when the real image arrives — always use the actual file ratio

The Fix, Step By Step

  1. 1

    Audit the page for images missing the attributes

    Paste the console snippet above into DevTools on each key template (home, blog post, product page). It prints every offending image with the exact width/height values to add. Lighthouse's own audit details list them too.

  2. 2

    Add width and height with the file's intrinsic dimensions

    Two attributes, integer pixels, no units: width="558" height="393". Any pair with the correct aspect ratio works — the ratio is what reserves the space.

  3. 3

    In CMS modules, read dimensions from the image field

    HubSpot: module.field.width / .height. WordPress: insert via the media library (automatic) and fix hand-written theme tags. Shopify: image.width / image.aspect_ratio. This keeps the attributes correct when editors swap images.

  4. 4

    Check module templates for ghost variables

    Search your theme's module code for size-attribute variables (HubSpot: sizeAttrs) and verify each <img> tag actually prints them — boilerplate frequently builds the variable and then omits it from the tag. Images inside modules (sliders, cards, tabs) never pass through the rich-text editor, so this is fixable only in the template.

  5. 5

    Confirm your CSS has the height:auto companion rule

    img { max-width: 100%; height: auto; } — this lets CSS control display size while the attributes control the reserved ratio. Without height:auto, an HTML height attribute can distort a CSS-resized image.

  6. 6

    Re-run Lighthouse and watch CLS

    The audit should clear, and if images were your main shifter, CLS drops with it. For the real-user verdict, check the Core Web Vitals section of PageSpeed Insights or Search Console after ~28 days of field data.

From the trenches

How we hit this on a real production site

This came out of the same enterprise B2B audit as our responsive-images work this week. The site's tab-switcher images shipped as bare <img> tags — no width, no height — so every tab image arrived as a layout surprise, and the audit list was long.

The fix rode along with the srcset upgrade we were already doing: each image got width/height attributes describing its intrinsic ratio, pulled from the HubSpot image fields so editors cannot break it by swapping images later. The two features are perfect partners — srcset/sizes picks the right FILE for each screen, width/height reserves the right SPACE before any file arrives.

The audit cleared on the next run, and the page stopped visibly assembling itself during load. Two attributes per image is the cheapest Core Web Vitals win there is — if your Lighthouse report lists this audit with a long stack of images, the console snippet above turns it into a fifteen-minute job.

Frequently Asked Questions

Will width and height attributes break my responsive layout?

No — this is the most common fear and it has been obsolete since 2019. When CSS sizes the image (max-width: 100%; height: auto), browsers use the attributes only to compute the aspect ratio for space reservation. CSS still controls the final rendered size at every breakpoint.

Do the attribute values have to match the displayed size?

No. They should describe the image file's intrinsic dimensions — or any pair with the same aspect ratio. A 1200x800 image displayed at 300px wide is correctly served by width="1200" height="800"; the browser reserves a 3:2 box and CSS scales it.

I use srcset with different file sizes — which dimensions do I put?

Any pair matching the shared aspect ratio (typically the largest file's dimensions). All srcset variants of one image should have the same ratio, so one width/height pair describes them all. If you art-direct different CROPS per breakpoint via <picture>, put matching width/height on each <source>.

What about images injected by JavaScript, sliders, or embeds?

Same rule, set at creation: include the attributes in the template the script renders, or set img.width / img.height before appending. For third-party embeds you cannot modify, wrap them in a container with CSS aspect-ratio to reserve the space externally.

Is CSS aspect-ratio a full replacement for the attributes?

Functionally close — aspect-ratio: 558 / 393 reserves space the same way. But the HTML attributes are self-documenting, survive CSS load failures, are what Lighthouse checks for directly, and require no stylesheet coordination. Use CSS aspect-ratio when you cannot touch the HTML; use the attributes everywhere else.

Still stuck? Send us the exact error.

Paste the exact error message you're seeing and where it happens. We'll take a look — if it's quick we'll point you at the fix, and if it's deeper we'll tell you honestly what it takes.

Reach the Summit

Rather have someone just fix it?

This is literally what we do all day — websites, SEO, performance, and the weird errors in between.

Start the Conversation