Skip to content
Back to Journal
Fundamentals

SVG Not Showing or Showing Black? 11 Causes and Fixes

SM
SVG Maker AI
15 min read

An SVG not showing — a blank box, a broken-image icon, a black blob — comes down to one of eleven causes, and each leaves evidence in DevTools: a status code, a response header, a console line or a 0 × 0 box. Start with the symptom table, then jump to the cause: each section has a minimal broken example, the check that proves it, and the fix. Wherever this post says “in our test”, that means Chromium 152 against a local test server.

SVG not showing? Start with the symptom

First ask how the SVG reaches the page: inline markup, an <img>, a CSS background, or <use>? Inline SVG goes through the forgiving HTML parser. Every other route loads the file as a separate XML image document, which forgives nothing and cannot see your page's CSS.

SVG not showing: symptom, likely cause and fix
SymptomLikely causeFix
Broken-image icon in <img>; fine when pasted inlineNo xmlns, or XML that does not parseAdd the SVG namespace; fix the error the browser reports
Renders when opened in a tab, broken in <img>Content-Type is not image/svg+xmlFix the server or CDN type mapping, then purge
Broken icon, red 404 row in the Network panelWrong pathRoot-relative URL; public/ is not in it
Works locally, 404 after deployFilename case, or a sub-path deployMatch case exactly; add the base path
Element is 0 × 0, or fills the whole columnviewBox but no width or heightSet width and height, as attributes or CSS
Right-sized box with nothing in itviewBox misses the art; fill="none" and no stroke; colour matches the backgroundCompare the viewBox with getBBox(); check computed fill and stroke
Solid blackNo fill, a lost <style>, or currentColor in an imagePresentation attributes; inline it to inherit colour
Gradient, clip path or mask missingDefs inside a display:none containerHide defs with a 0 × 0, absolutely positioned <svg>
Second icon wears the first one’s colours, or vanishes at a breakpointDuplicate ids or classes across inline SVGsPrefix ids per file
Sprite icon blank; console says domains, protocols and ports must match<use> pointing at another originServe the sprite same-origin, or inline it
Console names a Content Security Policy directiveCSP blocks data: images or inline stylesAdd data: to img-src; use presentation attributes
CSS background blank, same SVG fine elsewhereUnescaped #, quote or line break in a data URI%23 for #, or a small escaping helper
file says PNG, or it blurs when scaledNot a real vectorRe-export, redraw or trace

If the symptom does not point anywhere, run this in the console. It checks the four things a file-based SVG needs — a 200, the right Content-Type, XML that parses, and the SVG namespace — and tells you whether the file is secretly a PNG or a bitmap in a wrapper.

diagnose-svg.js
// Paste into the DevTools console on the page where the SVG fails.
// Top-level await works in the console. Use your own src for the URL.
const url = '/images/logo.svg'
const res = await fetch(url)
console.log('status', res.status, '| content-type', res.headers.get('content-type'))

const head = new Uint8Array(await res.clone().arrayBuffer()).slice(0, 4)
console.log('first bytes', [...head].map(b => b.toString(16).padStart(2, '0')).join(' '))
// 3c 73 76 67 is "<svg", 3c 3f 78 6d is "<?xm", 89 50 4e 47 is a PNG

const doc = new DOMParser().parseFromString(await res.text(), 'image/svg+xml')
const error = doc.querySelector('parsererror')
console.log(error ? 'XML error: ' + error.textContent : 'XML parses')

const root = doc.documentElement
console.log('namespace', root.namespaceURI, '| viewBox', root.getAttribute('viewBox'),
  '| width', root.getAttribute('width'), '| height', root.getAttribute('height'))
console.log('shapes', doc.querySelectorAll('path, rect, circle, ellipse, line, polyline, polygon, text').length,
  '| <image> elements', doc.querySelectorAll('image').length)

It uses fetch, so a file on another origin only answers if that server sends CORS headers. For a CDN URL, curl -I gives you the status and headers either way.

Missing xmlns, or XML that will not parse

This is the classic works-inline, broken-in-<img> bug. The HTML parser puts any <svg> tag into the SVG namespace for you, so markup copied out of a component renders inline without the declaration. Loaded as a file — through <img>, a CSS background or a data URI — the same markup is parsed as XML, where an <svg> with no namespace is just an unknown element. In our test, Chromium showed the broken-image icon.

logo.svg
<!-- logo.svg — renders when pasted inline in HTML,
     broken in <img>, in a CSS background and as a data URI -->
<svg viewBox="0 0 24 24" width="24" height="24">
  <circle cx="12" cy="12" r="10" fill="#16a34a"/>
</svg>

<!-- Fixed: the namespace makes it an SVG document, not generic XML -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
  <circle cx="12" cy="12" r="10" fill="#16a34a"/>
</svg>

The check: open the SVG's URL in its own tab. A drawing means the file is fine. Chrome's banner saying the XML file has no style information, with the tag tree underneath, means the namespace is missing. A page headed This page contains the following errors means the XML is malformed, and it gives the line and column.

The usual parse errors are things HTML forgives: a bare & in a <title> (write &amp;), an unclosed element, an unquoted attribute, or xlink:href with no xmlns:xlink declaration — switch that to plain href, which SVG 2 prefers. Paste the markup into the code panel of our SVG editor and its status bar shows whether the XML is valid; the export refuses markup that does not parse, and adds xmlns when it is missing.

The server sends the wrong Content-Type

An SVG file must arrive as image/svg+xml. Chromium does not sniff its way to SVG: in our tests the same valid file failed in <img> when served as text/plain, application/octet-stream and even application/xml. The last is the trap: opened directly in a tab, that copy rendered perfectly. Only the image route insists on the type.

The check: in the Network panel, click the request and read content-type under Response Headers. The usual culprits are an object store where the upload did not set a type, a hand-rolled static server, a CDN that cached a bad response, and an nginx config with its own types block.

fix-content-type (curl, nginx, Apache, S3)
# The check: the header must read image/svg+xml
curl -sI https://www.example.com/images/logo.svg | grep -i content-type

# nginx — the stock mime.types already maps svg and svgz.
# Make sure the http block includes it. A types { } block inside a
# server or location replaces the inherited map instead of adding to it.
http {
    include       mime.types;
    default_type  application/octet-stream;
}

# Apache (vhost or .htaccess)
AddType image/svg+xml .svg .svgz
AddEncoding gzip .svgz

# Amazon S3 — set the type when the object is uploaded
aws s3 cp logo.svg s3://your-bucket/images/logo.svg --content-type image/svg+xml

After fixing the origin, purge the CDN, or it will keep serving the cached header. Compression and caching for SVG are covered in our SVG performance guide.

Broken paths: 404s, the public folder and CSS-relative URLs

A 404 shows the broken-image icon in <img> and nothing at all in a CSS background, which is why background paths go unnoticed. Filter the Network panel by svg and look for red rows. Four rules cause most of them:

  • The public folder is the root. In Next.js and Vite, public/icons/logo.svg is served at /icons/logo.svg. The word public never appears in the URL.
  • Page-relative paths move with the page. icons/logo.svg works on / and 404s on /blog/post. Use root-relative paths — and if the site is deployed under a sub-path, such as a Next.js basePath, include that prefix too.
  • CSS resolves against the stylesheet. A relative url() is resolved against the stylesheet's URL, not the page's (MDN).
  • Case matters on the server. macOS and Windows file systems ignore case by default; Linux servers do not. Logo.svg requested as logo.svg works locally and 404s in production.
paths.html
<!-- On disk: public/icons/logo.svg -->
<img src="/public/icons/logo.svg" alt="">   <!-- 404: public/ is not part of the URL -->
<img src="icons/logo.svg" alt="">           <!-- works on /, 404s on /blog/post -->
<img src="/icons/logo.svg" alt="Acme">      <!-- root-relative: works on every route -->
site.css
/* This stylesheet is served from /assets/css/site.css */

/* Requests /assets/css/images/wave.svg — relative to the stylesheet */
.hero { background-image: url("images/wave.svg"); }

/* Requests /images/wave.svg */
.hero { background-image: url("/images/wave.svg"); }

Zero size, no viewBox, or a viewBox in the wrong place

An SVG with a viewBox but no width or height has a ratio and no size, so it takes its width from its container. In a plain block that is the whole column: the giant-icon bug. In a box that sizes to its content — an inline-flex button, an inline-block, an absolutely positioned badge — there is nothing to take, and Chromium collapsed it to 0 × 0 in all three. With neither, you get the 300 × 150 default box.

button.html
<!-- viewBox, no size: Chromium collapses this to 0 x 0 inside an
     inline-flex button, an inline-block or an absolutely positioned box,
     and stretches it to the full column width inside a plain block -->
<button class="btn">
  <svg viewBox="0 0 24 24">
    <path d="M5 12h14M12 5v14" stroke="currentColor" stroke-width="2"/>
  </svg>
  Add
</button>

<!-- Fixed: the viewport gets a size, the viewBox keeps the ratio -->
<button class="btn">
  <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
    <path d="M5 12h14M12 5v14" stroke="currentColor" stroke-width="2"/>
  </svg>
  Add
</button>

The other failure is a box of the right size with nothing in it, because the viewBox does not frame the artwork. Art drawn on a 24-unit grid inside viewBox="0 0 2400 2400" renders at a hundredth of its size; a shape at x="100" inside 0 0 24 24 sits outside the window entirely. The check: hover the element in the Elements panel to see its box, then compare the viewBox with the artwork's bounding box.

bbox-check.js
// Select the inline <svg> in the Elements panel; $0 is the selected node.
console.log('viewBox', $0.getAttribute('viewBox'))
const box = $0.getBBox()
console.log('artwork', box.x, box.y, box.width, box.height)
// Artwork outside the viewBox, or tiny next to it, means the viewBox is wrong.
// Set it to the artwork box, plus a little padding.

getBBox() needs a rendered inline SVG, so paste a file's markup into the page temporarily. The two boxes are explained in our viewBox guide. Our editor's export guarantees a viewBox, a width and a height (a missing viewBox is built from the width and height), but it does not check that the viewBox frames the art.

Drawn but invisible, or solid black: fill, stroke and currentColor

When the box is the right size and the Network panel is clean, the shapes are probably there, in a colour you cannot see. Select one in the Elements panel and read fill and stroke under Computed.

  • Stroke icons with no stroke: Lucide-style icons put fill="none" stroke="currentColor" on the root and draw only with strokes. Strip the stroke, or let a CSS reset set stroke: none, and nothing is left.
  • currentColor through an image: inline, it is the page's text colour. Through <img> or a CSS background the SVG is a separate document your CSS cannot reach, and currentColor resolved to black in our test — black lines on a near-black header.
  • A hairline: stroke-width="1" in a 1000-unit viewBox, shown at 24 px, is 0.024 px wide.
  • Camouflage: a white logo on a white card. Open the file in its own tab to check.
menu-icon.html
<!-- /icons/menu.svg: a typical stroke icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"
     fill="none" stroke="currentColor" stroke-width="2">
  <path d="M4 6h16M4 12h16M4 18h16"/>
</svg>

<!-- Loaded as an image, the icon cannot see color: #fff.
     currentColor resolves to black: black lines on #111. -->
<header style="background: #111; color: #fff">
  <img src="/icons/menu.svg" alt="Menu">
</header>

Inline the SVG when it must follow the text colour. Otherwise hard-code the colour, or use the file as a CSS mask over a currentColor background — see SVG as a CSS background image, and combining SVG with CSS for what page styles can reach.

Why an SVG shows up black

Black is the initial value of fill, so a black shape means the intended fill never applied. Three usual routes: the colour lived in a class whose <style> block was not copied with the shape (Illustrator-style .cls-1 exports); the file uses currentColor and is loaded as an image; or a Content Security Policy dropped inline styles. A presentation attribute fixes all three, because it travels with the shape.

black.svg
<!-- Solid black: the colour lived in a .cls-1 rule that was not copied -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
  <path class="cls-1" d="M2 2h20v20H2z"/>
</svg>

<!-- Fixed: a presentation attribute travels with the shape -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
  <path fill="#16a34a" d="M2 2h20v20H2z"/>
</svg>

Gradients and clip paths that vanish: display:none and duplicate ids

Gradients, patterns, clip paths, masks, filters and markers are used by reference — fill="url(#brand)" — and an inline reference resolves to the first element in the whole HTML document with that id. Two things go wrong.

Defs inside display:none

Hiding a defs block with display: none is the obvious move, and it breaks everything except symbols. In Chromium 152, gradients and patterns from a display:none container painted nothing; clip paths, masks, filters and markers were silently ignored; symbols used through <use> still worked. The SVG 2 spec says paint servers stay referenceable under display:none, but Chrome does not honour it, so take the container out of the layout instead.

defs.html
<!-- Broken in Chromium: the gradient sits in a display:none subtree,
     so the rect below paints nothing -->
<svg style="display: none">
  <linearGradient id="brand">
    <stop offset="0" stop-color="#16a34a"/>
    <stop offset="1" stop-color="#2563eb"/>
  </linearGradient>
</svg>

<!-- Fixed: keep the defs rendered, but out of the layout -->
<svg width="0" height="0" style="position: absolute" aria-hidden="true">
  <linearGradient id="brand">
    <stop offset="0" stop-color="#16a34a"/>
    <stop offset="1" stop-color="#2563eb"/>
  </linearGradient>
</svg>

<svg viewBox="0 0 24 24" width="24" height="24">
  <rect width="24" height="24" fill="url(#brand)"/>
</svg>

The same id twice

Inline two SVGs that both define id="a" and the second paints with the first one's gradient. If the first copy sits inside something hidden — the mobile menu's copy of the logo — the visible copy resolves to the hidden one and goes blank, which is how a logo vanishes only at some breakpoints. <style> blocks in inline SVGs collide the same way: each applies to the whole page, so two exports defining .cls-1 fight and the later wins. Optimised files are a common source: SVGO's default cleanupIds minifies referenced ids to short names, and its docs warn about this clash.

find-id-problems.js
// Lists ids inside inline SVGs that are defined more than once, or whose
// first definition is a gradient, pattern, clip path, mask, filter or
// marker sitting in a display:none subtree.
const REFERENCED = 'linearGradient, radialGradient, pattern, clipPath, mask, filter, marker'
const byId = new Map()
document.querySelectorAll('svg [id]').forEach(el => {
  byId.set(el.id, [...(byId.get(el.id) || []), el])
})
for (const [id, els] of byId) {
  const first = els[0]
  const hidden = first.matches(REFERENCED) && first.closest('svg').getClientRects().length === 0
  if (els.length > 1 || hidden) console.warn('#' + id, { copies: els.length, firstIsHidden: hidden }, els)
}

The fix is a unique prefix per file. SVGO's prefixIds plugin does it at build time, updating the references along with the ids:

svgo.config.mjs
// svgo.config.mjs — prefix every id and class with the file name,
// so two optimised files can share a page without colliding.
export default {
  plugins: [
    'preset-default',
    { name: 'prefixIds', params: { delim: '__', prefixClassNames: true } },
  ],
}

<use> pointing at a sprite on another origin

<use href="/sprite.svg#search"> loads an external sprite without complaint when it comes from the page's own origin. Point it at a CDN on a different host and Chromium refuses, logging Unsafe attempt to load URL followed by Domains, protocols and ports must match. Adding Access-Control-Allow-Origin to the sprite changed nothing in our test, and MDN notes there is currently no defined way to set a cross-origin policy for <use>. The same rule catches a page opened from disk, because Chrome treats file: URLs as unique origins. And data: URLs in <use> are blocked outright now — Chrome announced the removal, and our test confirmed it.

sprite-use.html
<!-- Page on https://www.example.com, sprite on a CDN: refused.
     An Access-Control-Allow-Origin header on the sprite does not help. -->
<svg width="24" height="24"><use href="https://cdn.example.com/sprite.svg#search"/></svg>

<!-- Same origin: works. A reverse-proxy path to the CDN counts. -->
<svg width="24" height="24"><use href="/sprite.svg#search"/></svg>

Three fixes, best first: serve the sprite from your own origin (a reverse-proxy path is enough), inline it into the page, or fetch it and inject it. Whether a sprite beats an icon font at all is covered in SVG icons vs icon fonts.

inject-sprite.js
// When the sprite has to stay on the CDN: fetch it (fetch does honour CORS)
// and put it in the page, so <use href="#search"> becomes a local reference.
// Only do this with a sprite you control — markup inserted this way can
// carry event-handler attributes, and nothing here strips them.
const res = await fetch('https://cdn.example.com/sprite.svg')
const holder = document.createElement('div')
holder.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden'
holder.setAttribute('aria-hidden', 'true')
holder.innerHTML = await res.text()
document.body.prepend(holder)

A Content Security Policy blocking it

CSP failures are the easiest to diagnose, because the console names the directive. Two show up with SVG. The first is data URIs: in our test, img-src 'self' blocked both <img src="data:image/svg+xml,…"> and a data: CSS background, while the same-origin file kept working. Add data: to img-src. An SVG loaded as an image runs no script (MDN), so this does not open a scripting hole.

csp-headers.txt
# Blocks <img src="data:image/svg+xml,..."> and url("data:...") backgrounds,
# because img-src falls back to default-src:
Content-Security-Policy: default-src 'self'

# Allows them:
Content-Security-Policy: default-src 'self'; img-src 'self' data:

The second is inline SVG under a strict style-src. Without 'unsafe-inline', both style attributes and <style> blocks inside inline SVG are dropped, and the shapes fall back to black. Presentation attributes are not CSS text, so CSP leaves them alone: in our test, fill="#16a34a" survived the same policy that stripped style="fill: #16a34a". There is no reason to loosen script-src for any of this; rendering never depends on it.

csp-inline.html
<!-- Under style-src 'self' (no 'unsafe-inline') both colours are
     dropped and the rects fall back to black -->
<svg viewBox="0 0 24 24" width="24" height="24">
  <rect width="24" height="24" style="fill: #16a34a"/>
</svg>
<svg viewBox="0 0 24 24" width="24" height="24">
  <style>.ok { fill: #16a34a }</style>
  <rect class="ok" width="24" height="24"/>
</svg>

<!-- A presentation attribute is not inline CSS, so CSP leaves it alone -->
<svg viewBox="0 0 24 24" width="24" height="24">
  <rect width="24" height="24" fill="#16a34a"/>
</svg>

Unescaped characters in a data URI

A data URI is a URL, so URL syntax applies to the SVG inside it. The # in a hex colour starts a fragment, which cuts the image off mid-attribute: the XML no longer parses and nothing paints. Write %23 instead (MDN lists what needs encoding). Two traps are CSS ones: a double quote inside url("…") ends the string early, and a raw line break inside a quoted url() is a syntax error — in our test it took every later rule in the stylesheet down with it. And a data URI without xmlns is blank.

data-uri.css
/* Blank: "#" starts the URL fragment, so the image ends at fill=' */
.bad {
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' fill='#16a34a'/></svg>");
}

/* Works: # written as %23, single quotes inside the double-quoted url() */
.good {
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect width='24' height='24' fill='%2316a34a'/></svg>");
}

Rather than hand-escaping, generate the URI. encodeURIComponent is the lazy safe choice, but it also escapes every space, slash, colon and equals sign. Across 19 distinct SVGs from our own codebase it made the markup 18–70% longer, and longer than base64 for seven of them. Escaping only the characters that break added 1–11%:

to-data-uri.js
// Escape only what breaks, so the result stays close to the original size.
// % goes first, so the escapes added after it are not escaped again.
// Assumes no single quotes inside attribute values (font-family="'Inter'").
const toDataUri = svg => 'data:image/svg+xml,' + svg.trim()
  .replace(/"/g, "'")       // fits inside url("...")
  .replace(/%/g, '%25')
  .replace(/#/g, '%23')     // hex colours, url(#id)
  .replace(/</g, '%3C')
  .replace(/>/g, '%3E')
  .replace(/\s+/g, ' ')     // no raw line breaks in the CSS string

const markup = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <rect width="24" height="24" fill="#16a34a"/>
</svg>`

document.querySelector('.hero').style.backgroundImage = 'url("' + toDataUri(markup) + '")'

Our editor's Copy Data URI button emits base64 instead, which leaves nothing to escape, at a cost in size: base64 spends four characters on every three bytes, so it runs about a third larger than the markup it encodes. The full escaping rules are in SVG as a CSS background image.

Not really an SVG: a renamed PNG, or a lone <image>

Renaming logo.png to logo.svg does not make a vector. Served as image/svg+xml, the PNG bytes are parsed as XML and fail on the first byte. The diagnose snippet reports 89 50 4e 47 as the first bytes, and on macOS or Linux file names it at once:

terminal
$ file logo.svg
logo.svg: PNG image data, 24 x 24, 8-bit/color RGB, non-interlaced

The subtler fake is a real SVG whose only content is an <image>: a bitmap in a wrapper. If its href points at an external file, it renders blank in <img>, because an SVG loaded as an image cannot fetch external resources. Embedded as base64 it renders, but it is still a bitmap: it blurs when scaled up, and a cutting machine finds no paths to cut.

wrapper.svg
<!-- A "vector" that is a bitmap in a wrapper. Loaded through <img>,
     the external href is never fetched, so this renders blank -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
  <image href="logo.png" width="512" height="512"/>
</svg>

Where to go next

Four of these causes — the namespace, parse errors, a missing viewBox and a missing size — can be stopped at export. Paste your markup into the SVG editor's code panel and the status bar tells you whether the XML is valid; the export refuses markup that does not parse, and writes xmlns, a viewBox, a width and a height onto the file. It will not fix a server header, an id collision or a viewBox that misses the art. The SVG optimiser strips editor namespaces and metadata from design-tool exports, though it does not prefix ids. For cut files, see making SVG files for Cricut.

For the background: what an SVG file is, the viewBox guide for sizing, SVG in Next.js for imports and the public folder, and the performance guide for serving and compression.

Frequently asked questions

Why is my SVG not showing?
Usually one of a handful of causes: the file has no xmlns="http://www.w3.org/2000/svg" or contains invalid XML, the server sends a Content-Type other than image/svg+xml, the path returns a 404, the SVG has a viewBox but no width or height and collapses to zero, or the shapes are drawn in a colour you cannot see. Open the file's URL in its own tab, then check the status and content-type in the Network panel; those two checks rule out most of them.
Why does my SVG show up as black?
Black is the default fill, so a black shape means the colour you intended never applied. The usual reasons: the colour lived in a CSS class whose style block was not copied with the shape, the file uses currentColor and is loaded through an img tag where page CSS cannot reach it, or a Content Security Policy blocked inline styles. Setting the colour as a fill attribute on the shape itself fixes all three.
Why does my SVG work inline but not in an img tag?
Inline SVG is read by the forgiving HTML parser, which adds the SVG namespace for you. Through an img tag, a CSS background or a data URI, the file is parsed as strict XML: it needs xmlns="http://www.w3.org/2000/svg", must be well-formed, must be served as image/svg+xml, cannot load external images or stylesheets or run scripts, and cannot see your page's CSS, so currentColor resolves to black.
Can I open an SVG file in Chrome?
Yes. Drag the file into a Chrome window or open its URL, and Chrome renders it if it is valid. A page saying the XML file has no style information, with a tag tree below, means the SVG namespace is missing. A page headed 'This page contains the following errors' means the XML is malformed, and it gives the line and column. If the file is really a renamed PNG, the file command on macOS or Linux names the real format.
Why does my SVG work locally but not after deploying?
Three usual causes. Filenames: macOS and Windows ignore case by default and Linux servers do not, so Logo.svg requested as logo.svg returns a 404 in production. Headers: the production server or CDN may send a Content-Type other than image/svg+xml, which breaks img tags. Origins: a sprite on a CDN referenced with use is refused when the CDN is a different origin, even with CORS headers. The Network panel and console show which one it is.
Why is svg use not working?
Check three things. The fragment must match a real id in the sprite, such as /sprite.svg#search for a symbol with id="search". The sprite must come from the same origin as the page: browsers refuse cross-origin use references, CORS headers do not change that, and data: URLs are blocked too. And the symbol needs a viewBox and the outer svg a width and height, or the icon renders at the wrong size.
Why is my SVG gradient not showing?
Most often the gradient is defined inside an svg hidden with display:none, which Chrome does not paint from, or its id is duplicated elsewhere on the page, so the reference resolves to a different or hidden copy. Hide shared definitions in a zero-size, absolutely positioned svg instead, and give each file's ids a unique prefix, for example with SVGO's prefixIds plugin.
How do I check if an SVG file is valid?
Open it directly in a browser tab: a rendered drawing means it parses, and an error page gives the line and column of the first XML error. In the DevTools console, DOMParser with the image/svg+xml type returns a parsererror element for broken markup. Pasting the markup into the SVG Maker AI editor's code panel also shows whether the XML is valid in its status bar.
ShareLinkedInXReddit