lucky.graphics

guides

SVG Not Showing? Find the Cause With One Test Each

Why an SVG shows in the browser but not on your site: a symptom-to-cause table covering MIME type, xmlns, size, colour, img limits, CSP and CMS uploads.

Published
November 13, 2026
Topic
guides
Reading time
17 min
Review
Reviewed against the primary source

SVG Not Showing? A Symptom-to-Cause Table With One Test Per Cause

An SVG that opens fine as a file but stays blank on your page has failed at one of five layers: the server response (wrong Content-Type, a 404, a blocking Content-Security-Policy), the file itself (no xmlns, an XML error), its size, its colour, or the embed method, where an img tag refuses fonts, stylesheets and cross-origin references. Each layer has a test that takes under a minute.

The server layer catches more people than any other, because SVG has no fallback there. The WHATWG MIME Sniffing Standard lists the byte signatures browsers may use to recognise an image whose label is wrong, and that table holds six types: ICO, BMP, GIF, WebP, PNG and JPEG. SVG is not one of them. A PNG sent with a careless Content-Type can still be recognised from its first bytes; an SVG is plain XML text, and when the label says something other than image/svg+xml the browser has no signature to check it against.

The Table: What You See, What Broke, How to Prove It

What you seeMost likely causeOne-line testFix
Broken-image icon or alt text where an img should beThe file 404s, or the server sends a Content-Type other than image/svg+xmlNetwork panel: check the status code and the Content-Type response header for the SVG requestFix the path (case matters on most servers), or map .svg to image/svg+xml in the server or host config
Works when you open the file on your computer, fails on the live siteSame as above: your desktop knows the .svg extension, your server may notOpen the SVG's live URL directly in a tab; a download prompt or raw text means a bad Content-TypeCorrect the server's MIME mapping
The live URL shows raw XML text or an XML tree instead of a pictureThe root svg element has no xmlns="http://www.w3.org/2000/svg"Search the file's first line for xmlnsAdd the SVG namespace to the root element
The live URL shows an XML parse error with a line and columnThe file is not well-formed XML: an unescaped ampersand, an unclosed tag, a stray character before the rootOpen the URL directly; the error names the line and columnEscape & as & and fix the reported position
Nothing visible, but the element takes spaceFill and stroke resolve to nothing visible, or to black on a black backgroundTemporarily give the page or wrapper a white background, or set a fill colour in the fileSet fill or stroke explicitly in the file; do not rely on page CSS reaching an img
An icon that follows text colour when inline turns black inside an imgcurrentColor resolves to the SVG document's own colour, not the page'sSwap the img for the same markup inline; if the colour returns, this is the causeInline the icon, or hard-code the colour in the file used as an image
The image box is 300 by 150 pixels, or collapses, or the drawing sits in a cornerNo width and height on the root and no viewBox, so the file has no natural size and no ratioCheck the root element for width, height and viewBoxAdd a viewBox; add width and height for a natural size
Text in the SVG falls back to a default font, or vanishes, only inside img or CSS backgroundsExternal fonts and stylesheets are not fetched when SVG is used as an imageOpen the SVG URL directly: if the font appears there, the image sandbox is blocking itConvert text to outlines, or embed the font as a data URL
A use element that points at a sprite on a CDN draws nothingCross-origin use references may be refused, and there is no CORS opt-in for useCopy the sprite to the page's own origin and retryServe sprites from the same origin, or inline them
A data:image/svg+xml background works with named colours, breaks with hex coloursThe # in a hex colour starts the URL fragment and cuts the data offSearch the data URL for an unencoded #Encode # as %23, or encode the whole SVG
Blank image plus a console message mentioning Content Security Policyimg-src, or default-src when img-src is absent, does not allow the SVG's sourceRead the console: it names the directive and the blocked URLAdd the origin, or data: for data URLs, to img-src
The upload is rejected, or the published file differs from the one you uploadedThe CMS does not accept SVG by default, or a sanitizer rewrote the fileDownload the published SVG and compare it with your originalUse the CMS's supported SVG route and check what its sanitizer removes

Most rows can be told apart with one move: open the SVG's own URL directly in a browser tab. If it renders there and not on your page, the file and the server are fine, and the cause is the embedding, the CSS around it, or a page policy. If it fails there too, the problem is the response or the file, and the page is innocent.

Start With the Direct URL and the Network Panel

Every SVG on a page is a separate HTTP request unless it is inline markup, and the browser developer tools show that request in full. Two fields settle most cases.

The status code comes first. A 404 shows as a broken image, and the classic version of it is a path that works on a Windows or macOS laptop and fails on a Linux server because Icon.svg and icon.svg are different files there. A path written in a CSS file is a second trap: MDN's url() reference says "Relative URLs are resolved relative to the stylesheet URL, not the web page URL." A background SVG referenced as images/arrow.svg from /assets/css/site.css is requested from /assets/css/images/arrow.svg, wherever the page itself lives.

The Content-Type header comes second, and it must read image/svg+xml, the type MDN's common MIME types list gives for .svg. Any other value is a defect, even when one browser happens to tolerate it. The reason sits in the sniffing standard quoted above: its image context rule says that when the supplied type is an XML MIME type, the computed type is simply the supplied type, and its signature table has no SVG row to fall back on. The browser trusts the label you send because there is nothing else it can check.

To read the header without a browser, ask the server for it from a terminal:

curl -sI https://example.com/images/logo.svg | grep -i content-type

The answer should be one line containing image/svg+xml. A response labelled text/plain, application/octet-stream or text/html is the bug, and the fix belongs in the server configuration, the hosting platform's headers file, or the object storage bucket's metadata, which some upload tools set from a guess or leave blank.

Local development servers usually get this right, which is why a site can work on a laptop and fail after deployment. As one check, Python's standard mimetypes module (the table behind its built-in http.server) was asked here for the two SVG extensions. With Python 3.13.18 it returned image/svg+xml with no encoding for icon.svg, and image/svg+xml with the encoding gzip for icon.svgz. That second answer matters if you ship compressed .svgz files: the type stays image/svg+xml, and the server must also send a Content-Encoding: gzip header, or the browser receives compressed bytes labelled as XML text and cannot parse them.

A note on X-Content-Type-Options: nosniff, since it is often blamed. MDN's reference says the header blocks responses on a MIME mismatch for requests whose destination is script or style, and for other responses it makes the browser use the supplied Content-Type as-is. MDN does not name images in either rule. For an SVG the practical result is the same as without the header: the label has to be right.

The XML Parser Has No Forgiveness Mode

An SVG file is XML, and that changes the rules compared with the HTML around it. HTML parsers repair mistakes and keep going. XML parsers stop. The XML 1.0 specification (Fifth Edition) defines a fatal error as one a conforming processor must detect and report, and says that once one is found the processor "MUST NOT continue normal processing". In a browser that means no partial render: one bad byte, and the whole image is gone.

The missing namespace

The most common file-level failure is a root element written as a bare svg tag with no namespace. Inside an HTML page that markup works, because the HTML parser places an inline svg element in the SVG namespace on its own. MDN's namespaces guide puts it this way: for SVG, HTML and MathML in HTML documents "the namespace is implied and therefore optional", but "It is required to declare the namespace for XML files. If you don't, user agents will not recognize the content and will show the XML markup or inform the user that there's an error in the XML."

That is how the bug is usually born. Someone copies a working inline icon out of a component, saves it as a .svg file, and the file renders nowhere, because the namespace the HTML parser used to add is no longer being added.

The difference is visible to any XML parser. This desk wrote two one-path files and parsed both with Python's standard xml.etree module (Python 3.13.18):

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 4h16v16H4z"/></svg>
<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z"/></svg>

The first file, 91 bytes, parsed to a root element named {http://www.w3.org/2000/svg}svg, the SVG element in the SVG namespace. The second, 56 bytes, parsed to a root element named plain svg, in no namespace at all. Both are well-formed XML, so the parser reports no error. The second is an unknown element that happens to be called svg, and a browser renders nothing for it. The 35 bytes of the namespace declaration are the difference between an image and an empty box.

The test is a text search: open the file in an editor and look for xmlns="http://www.w3.org/2000/svg" on the root element. If the file uses xlink:href anywhere, it also needs xmlns:xlink="http://www.w3.org/1999/xlink" on the root. A 68-byte test file with a use element carrying xlink:href and no xlink declaration failed the same Python parse with "unbound prefix: line 1, column 40". The better fix for new files is to replace xlink:href with plain href, which MDN's use reference calls the SVG 2 replacement: "Since SVG 2, the xlink:href attribute is deprecated in favor of href."

One ampersand

The second file-level failure is a character that is legal in HTML text and illegal in XML text. The commonest is a bare ampersand in a title, a desc, a text element or a font name. To show it, this desk wrote a 124-byte file whose title read Terms & Conditions and parsed it with the same Python module. It failed with "not well-formed (invalid token): line 1, column 74", which points at the character after the ampersand. The same file with the ampersand written as &amp; (128 bytes) parsed cleanly. Browsers report the same kind of error, with a line and column, when you open the broken SVG's URL directly, which is one more reason to start with the direct URL.

Other fatal errors to check for in hand-edited or generated files: an unclosed element, two attributes with the same name on one element, a less-than sign inside text, and anything before the XML declaration when the file has one. A single space before it was enough here: the parse failed with "XML or text declaration not at start of entity: line 1, column 1". A UTF-8 byte-order mark in the same position is allowed, and the test file with one parsed cleanly, so an editor that adds a BOM is not the cause. Template engines that build SVG from strings are the usual source: a user name or a product title containing an ampersand, inserted without escaping, turns one record's image blank while every other record works.

When the Image Loads but Takes the Wrong Space

A size failure is the confusing one, because the request succeeds, the file is valid, and the image is still wrong: a 300 by 150 pixel box where you expected a 24 pixel icon, a drawing stuck in the top-left corner of a larger box, or an element that seems to have no size at all.

The rules sit in SVG 2's section on intrinsic sizing. On the root svg element, "If either width or height are not specified, the used value is the initial value 'auto'", and "'auto' and percentage lengths must not be used to determine an intrinsic width or intrinsic height." The aspect ratio comes from absolute width and height if both are given, otherwise from a correctly specified viewBox, and otherwise "there is no intrinsic aspect ratio." MDN's img reference states the practical consequence: SVG images "have no intrinsic dimensions if their root svg element doesn't have a width or height set on it."

So there are three kinds of SVG file, as far as an img tag is concerned.

Root element carriesNatural sizeAspect ratioWhat an img with no CSS size shows
width, height and viewBoxYes, from width and heightYesThe drawing at its stated size, scalable by CSS
viewBox onlyNoYes, from the viewBoxA box sized by the layout, with the drawing scaled to fit the ratio
Neither, or percentage width and heightNoNoThe browser's fallback box, with the drawing unscaled

The fallback box in the last row comes from CSS 2.1. For an inline replaced element with no intrinsic width, "the used value of 'width' becomes 300px", and with no intrinsic height or ratio, the height comes from the largest 2:1 rectangle that "has a height not greater than 150px". That is where the mysterious 300 by 150 box comes from. Layouts that size items from their content, such as shrink-to-fit flex items or absolutely positioned wrappers, can produce an even smaller box when the file offers neither a size nor a ratio to work from.

The missing viewBox has a second effect beyond sizing. The viewBox is what creates the transformation that maps the drawing's coordinates onto whatever box the element ends up with; SVG 2 notes that without one, preserveAspectRatio is ignored. Worked through with stated inputs: an icon drawn in a 24 by 24 unit space, saved with width="24" height="24" and no viewBox, then displayed at 48 by 48 pixels by CSS. With no viewBox the 24 units stay 24 pixels, so the drawing fills a 24 by 24 square in the top-left of a 48 by 48 box: 576 of 2,304 square pixels, one quarter of the area, with three quarters empty. Add viewBox="0 0 24 24" and the same CSS scales the drawing to fill the whole box.

The test is to read the root element: if it has no viewBox, add one that matches the coordinate space the drawing uses (usually the original width and height, starting at 0 0). The SVG optimization guide lists the viewBox first among the things an optimizer must never remove, and a missing one after an optimization pass is the reason to check that tool's settings.

Loaded, Sized and Still Invisible: Colour

A shape with no fill and no stroke settings draws in black. MDN gives the default value of fill as black for every basic shape, path included, and the default of stroke as none. That default explains three different invisible-image reports.

The first is a black icon on a dark background. The file never set a colour, the default black was used, and on a dark header or in a dark theme it is black on near-black. The image is there; select it in the developer tools and its box is outlined. The dark mode design guide covers choosing icon colours for both themes, and the fix starts with a colour the file sets on purpose.

The second is a stroke icon that lost its stroke. Line-icon sets draw with stroke and set fill="none" on the root, so the shapes have no fill on purpose. If an edit or an optimizer removes the stroke attribute, the default of none applies, and every path draws with no fill and no stroke: a perfectly sized, perfectly empty image. The test is to search the file for stroke=; if a stroke icon has none, restore it from the source file.

The third is currentColor used as an image. Interface icons often set fill="currentColor" so they follow the text colour of the button or link they sit in, and inline in HTML that works: MDN defines currentColor as "the value of an element's color property", and color is inherited down from the page. Loaded through an img tag or a CSS background, the same file is a separate document. The page's stylesheets do not reach into it, so color takes its initial value, which MDN lists as canvastext. In a light colour scheme that system colour is normally black, so the icon draws black no matter what colour its button text is. The test is to paste the same markup inline: if it takes the text colour there and turns black in an img, currentColor is the cause. Either keep the icon inline, or save a copy with a fixed colour for image use.

What an img Tag Refuses to Do

An SVG loaded as an image runs in a restricted mode, and most "works on its own, broken on my page" reports that get past the server and the file end here. The W3C's SVG Integration draft (First Public Working Draft, 17 April 2014, still the reference for these modes) defines the processing modes. An SVG referenced from an HTML img element or a CSS image property uses the secure animated mode when its animations can run and the secure static mode when they cannot. Both are "not allowed to resolve external references", and in both, scripts are disabled. When an external reference is disabled, the fetch "must instead be treated as if a network error occurred and no data was received", a rule the draft applies to nested images, font-face sources, feImage and "any other network requests made by script, HTML, CSS or other Web platform features."

MDN's SVG-as-an-image guide gives the same limits in practical terms: JavaScript is disabled; external resources such as images and stylesheets "cannot be loaded, though they can be used if inlined through data: URLs"; :visited link styles are not rendered; and platform-native widget styling is disabled. It adds that these restrictions are specific to image contexts and "don't apply when SVG content is viewed directly, or when it's embedded as a document via the iframe, object, or embed elements." That sentence is the whole explanation for the "shows in the browser but not on the website" query: opening the file directly is not an image context.

What each embed method allows, from those two sources:

Embed methodPage CSS styles its insidesExternal fonts and stylesheets loadScripts runGood for
img elementNoNoNoLogos, illustrations and diagrams used as pictures
CSS background-image or maskNoNoNoDecorative patterns and simple icons
Inline svg markup in the HTMLYesThrough the pageThrough the pageIcons that follow text colour, animated or interactive graphics
object, iframe or embedNo, it is its own documentYesYesRare: a standalone SVG document that needs its own resources

The font row deserves its own test, because it fails quietly. An SVG exported with live text that names a web font, or one that links a Google Fonts stylesheet from inside the file, looks correct when opened directly from your machine (where the font may be installed) and falls back to a default font inside an img on another machine. Open the live URL directly on a computer that does not have the font installed: if the text is correct there and wrong in the img, the image sandbox is the cause. The fixes are to convert text to outlines in the design tool before export, which the vector file formats guide walks through for each export route, or to embed the font file inside the SVG as a data URL. Outlines are simpler and usually lighter for a short logo wordmark; embedding keeps text selectable but adds the whole font's weight to the image. If the font fails on the page itself, not just inside an SVG, the web fonts troubleshooting guide covers @font-face failures.

Sprites and the use element

A sprite file holds icons as symbol elements, and pages draw them with a use element whose href points at sprite.svg#icon-name. Two things break this. The first is origin. MDN's use reference says: "For security reasons, browsers may apply the same-origin policy on use elements and may refuse to load a cross-origin URL in the href attribute. There is currently no defined way to set a cross-origin policy for use elements." Moving a sprite to a CDN on another hostname is enough to blank every icon, and unlike images or fonts there is no CORS header to fix it. Serve the sprite from the page's own origin, or inline it in the HTML.

The second is styling. The use element clones the symbol into a hidden tree, and MDN warns that "CSS properties are not guaranteed to be inherited by the cloned DOM unless you explicitly request them using CSS inheritance." A sprite icon whose paths set their own fill ignores the colour you put on the use element. Setting fill="currentColor" (or no fill) on the symbol's paths, and the colour on the use element or its parent, lets inheritance carry the value in.

Data URLs and the # character

Small icons are often written straight into CSS as data:image/svg+xml URLs, and they break the moment a hex colour appears. In a URL, # starts the fragment. To check exactly what happens, this desk parsed a 125-character data URL containing fill="#e11d48" with Node.js v22.23.4's built-in WHATWG URL parser. The path, the part a browser treats as the image data, ended at fill=" (86 characters), and everything from #e11d48 onward, including the closing tags, was moved into the fragment. The image the browser decodes is therefore cut off mid-element, which is an XML fatal error. Replacing # with %23 left the fragment empty and the full markup in the path. MDN's own url() example encodes its colours this way (stroke='%2300F'), and also uses single quotes inside the SVG so they do not clash with the double quotes around the URL.

Policies That Block a Valid File

Content-Security-Policy

A page with a Content-Security-Policy header decides which sources images may come from. MDN's img-src reference says the directive "specifies valid sources of images and favicons", and that if it is absent the browser looks for default-src instead. An SVG from an origin the policy does not list is blocked, and the browser writes a console message naming the directive and the blocked URL, so the test is simply to read the console.

Data URLs are the frequent surprise. A policy of img-src 'self' allows images from the page's own origin, and MDN describes 'self' in exactly those terms. A data: URL is not on any origin. The CSP Level 3 draft (Working Draft, 27 October 2026) summarises its matching rules as requiring "explicit presence of any non-HTTP(S) scheme", unless the protected resource uses that same scheme. In practice, an inline data:image/svg+xml background that works on a site with no policy goes blank once a strict policy is added, until data: is listed in img-src. Add it only to img-src, not to script directives.

CMS uploads and sanitizers

Content management systems treat SVG with caution because an SVG file can carry script when it is opened directly, outside an image context. WordPress is the clearest documented case: the list of file types returned by its wp_get_mime_types() function, read on the WordPress developer reference, includes JPEG, GIF, PNG, BMP, TIFF, WebP, AVIF, ICO and the HEIC and HEIF family, and has no svg or image/svg+xml entry. An SVG upload to a default install is refused before any rendering question arises. Plugins and other platforms that do accept SVG usually pass it through a sanitizer, and the sanitizer decides which elements and attributes survive.

That makes the published file a different file. The one-line test is to download the SVG from its live URL and compare it with the one you uploaded, in a diff tool or by eye in a text editor. Look for removed style elements, use references, xlink attributes, foreignObject blocks and embedded fonts, since those are the parts a cautious sanitizer is most likely to treat as risky. If the comparison shows a stripped part your graphic needs, rework the file so it does not need it (plain attributes in place of a style block, outlined text in place of an embedded font) rather than turning the sanitizer off.

A Triage Order That Settles Most Cases

When a report says only "the SVG is not showing", running the checks in this order means each one either finds the cause or rules out a whole layer.

  1. Open the SVG's live URL directly. Renders: skip to step 5. Raw XML, a parse error or a download: steps 2 to 4.
  2. Read the status and Content-Type. 404 means a path problem; any type other than image/svg+xml means a server mapping problem; .svgz also needs Content-Encoding: gzip.
  3. Read the root element. It needs xmlns="http://www.w3.org/2000/svg", plus xmlns:xlink if xlink:href appears anywhere.
  4. Read the parse error, if there is one. Go to the line and column it names; an ampersand or an unclosed element is the usual culprit.
  5. Look at the element's box in the developer tools. 300 by 150, collapsed or cornered: check width, height and viewBox. A correct box with nothing visible: check fill, stroke and the background behind it.
  6. Compare the embed method with what the file needs. Fonts, linked stylesheets, nested images and currentColor theming do not work inside an img or a CSS background.
  7. Read the console. A Content Security Policy message names the directive to change.
  8. Diff the published file against your original if a CMS or upload pipeline handled it.

The order runs from what the server sends to what the page allows, so a fix at one step is never undone by a cause further down. Two causes can stack: a CMS can sanitize a file and then serve it with the wrong type. Re-run the direct URL test after each fix.

Files That Survive Every Embed

Most of the failures above can be designed out before a file is shipped. Give every delivery SVG the SVG namespace, a viewBox, and a width and height that state its natural size. Set colours in the file unless the icon is meant to be inline and follow text colour, in which case ship it inline. Outline text in logos and diagrams used as images. Keep sprites on the page's own origin. Encode # in data URLs. Keep the editable master separately and treat the delivery file as an export, since the optimization pass that trims the file is also where viewBoxes and strokes get lost.

The format choice comes before any of this: if the artwork is a photo or a texture, the image formats guide explains why a raster format will serve it better than an SVG wrapper. For a site icon, SVG is only part of the set, because some consumers do not read it, as the favicon sizes and formats guide explains. When the SVG is meaningful content rather than decoration, its text alternative belongs in the img alt attribute, and the alt text guide covers when to write one and when to leave it empty.

One habit catches the regressions: after any optimizer or build step touches an SVG, open the optimized copy's live URL and run steps 1 to 3 of the triage list again before it replaces the original. A stripped namespace, viewBox or stroke shows up there in seconds, long before a user reports a blank icon.

Questions Readers Ask About Blank SVGs

Why does my SVG show in the browser but not on my website?

Opening a file directly is not an image context, so none of the img restrictions apply, and your computer identifies the file from its extension. On the website, the server must label it image/svg+xml, and an img or CSS background blocks external fonts, stylesheets and scripts. Open the live URL directly: if it renders there, the cause is the embed or a page policy; if it fails, the server or the file is at fault.

Why is my SVG not displaying in an img tag when it works inline?

Inline markup gets two things for free that a file in an img does not. The HTML parser adds the SVG namespace, and page CSS can style the drawing, including the colour behind currentColor. Check the file for xmlns="http://www.w3.org/2000/svg", give it explicit fill or stroke colours, and outline any text that relies on a web font.

Can I style an SVG loaded with img from my page's CSS?

No. An SVG used as an image is its own document, and the page's stylesheets do not apply inside it. CSS can still size, position, crop and filter the img element as a whole. If you need to change colours on hover or per theme, inline the SVG, use a sprite with a use element on the same origin, or ship separate files for each colour.

My SVG works in one browser and not another. Where do I start?

Start with the Content-Type and the namespace, since both are defects even if one browser appears to cope, and then the use element's href: MDN says browsers may refuse cross-origin references, so a sprite on another host can work in one browser and fail in the next. Inside a standalone file, xlink:href without a declared xlink namespace is an XML error everywhere. Test with the file's live URL in each browser before looking at your CSS.

Does gzip or Brotli compression break SVG files?

Not when the server does it correctly. Compressed transfer is normal for SVG, and the browser decompresses it when the response carries a matching Content-Encoding header. Problems appear when a pre-compressed .svgz file is served with no Content-Encoding header, or when a file is compressed twice. The direct URL test shows garbled characters or a parse error in either case.

How do I get my CMS to accept SVG uploads?

Use the route your platform documents for SVG, which usually means a plugin or setting that adds the type and runs a sanitizer. A default WordPress install does not list SVG among its allowed file types. Whatever you use, download the published file afterwards and compare it with your original, since the sanitizer may have removed parts your graphic depends on.

Sources Checked on November 13, 2026