UPDATE: I made a improved version that you should probably use instead.

The key is SVG has the interesting element <switch>:

The switch element evaluates the requiredFeatures, requiredExtensions and systemLanguage attributes on its direct child elements in order, and then processes and renders the first child for which these attributes evaluate to true. All others will be bypassed and therefore not rendered.

It's an element that, well, switches between its child elements depending on the viewer's supported SVG features, or the user's language settings.

The trick is to not bother with any of the "conditional processing attributes":

  <switch>
  <g> <!-- I get rendered! --> </g>
  <g> <!-- I don't. --> </g>
</switch>

In browsers that understand inline SVG, they will render the first element where they meet the qualifications, and since the first element is unqualified, it always gets rendered.

So the first step, then, is to set up your inline SVG with an unqualified <switch> at the root, like this:

  <svg>
  <switch>
    <g> <!-- your beautiful inline SVG graphics here --> </g>
    <foreignObject> <!-- your fallback HTML here --> </foreignObject>
  </switch>
</svg>

Capable browsers won't show anything inside the fallback <foreignObject>.

Fixing things for modern browsers

However, their preload scanners, being not so smart, will still request any images they find in there. Considering your Scalable Vector Graphics fallback is likely to be an image, this might be an unacceptable performance hit. If so, consider the following egregious hack:

  <foreignObject>
  <img src="1x1.gif" style="background-image: url('real-image-here.png')"
       width="300" height="200"
       alt="accessibility matters, especially for older browsers!">
</foreignObject>

SVG-capable browsers won't put the fallback <img> in the render tree and therefore won't request its background-image, and their preloaders will only grab the tiny, ultra-cacheable 1-pixel GIF. I experimented with omitting the src entirely, or setting it to //:0, but that sort of thing has odd side effects at best, and at worst can result in broken image icons or visible alt text over the background image. Data URIs don't have much better browser support than SVG, either.

If the inline SVG is being used for purely decorative things, you could probably replace the stretched-GIF image with a regular old <div> and prevent the preloader request. But be sure you're not locking anyone out who has an older browser and a visual disability; for a long, long time, screen-readers only worked well with Internet Explorer, so IE8 has a disproportionate market share.

In my case, the inline SVGs are the main content of the site, so I went the extra mile and wrote a script that would loop through document.images and set the background image's URL to the fallback's srcs, to allow for things like right-clicking and saving the image, and the ability to see the content on proxy browsers like Opera Mini (which ignore background images to save bandwidth). It's JavaScript so old that even Netscape Navigator can run it, so as a minor enhancement, it seemed worth it.

(Though, Opera Mini was recently upgraded to handle inline SVG, so I've been rethinking the need for this script.)

Cleaning up the mess in older browsers

If your inline SVG is just a bunch of <rect>s and <path>s and other empty graphical elements, then you've got no more work to do. But some SVG elements can show up as shadows of themselves in older browsers. The troublemakers are:

  • Embedded images: <image>
  • Textual elements: <desc> & <text>
  • Meta elements you shouldn't be using in inline SVG anyway: <script>, <style>, <metadata>, etc. They all have HTML equivalents that should be used instead.

Hiding <image> elements

SVG lets you embed other images by using the <image> element. It looks like this (the width and height attributes are required):

  <image xlink:href="image.png" width="300" height="200" />

And as it turns out, browsers since early Netscape have been aliasing any <image>s they find to <img>, because broken markup lives forever. So those poor old SVG-incapable browsers are going to think you meant this (with some useless extra attribute it doesn't understand):

  <img width="300" height="200">

And it will dutifully carve out 300 by 200 pixels to display a broken image icon in. The fix is thankfully pretty easy:

  <image class="svg-image" ... />

  .svg-image {
  width: 0;
  height: 0;
}

The old browsers will hide the ghost image entirely, and setting width and height in CSS on SVG elements doesn't do anything for the capable ones:

(By the way, if a light bulb went off in your head and you figured you could give <image> a src and have that work in old and new browsers, it's been tried and it has some issues you should know about.)

Hiding textual elements

This one is trickier. Most elements in SVG don't contain anything in between their brackets, all their data is stuffed into attributes. This was apparently done for backwards compatibility, which was considered high treason by the XMLites:

Is this requirement really "an SVG document should display its textual content if loaded into a browser that supports only HTML"?

Thank goodness for that, huh?

At any rate, there are only a few elements in SVG that accept character data inside:

  • <title>
  • <desc>
  • <metadata>
  • <script>
  • <style>
  • <text>

<script> and <style> shouldn't really be used in inline SVG, but if they sneak into there, old browsers will treat them like usual. Anything you would put in <metadata> would probably be better off in the <head>. And old browsers will think you don't know how to HTML if they find <title> hanging out in the <body>, so they'll just discard it silently.

So the problematic elements are <desc> and <text>. It used to be that <desc> would auto-switch to HTML inside inline SVG, which meant we could do this:

  <desc><div style="display: none"> <!-- content --> </div></desc>

But apparently that's changed in the spec, which is a damn shame. I tried anyway, but the validator agrees; not allowed. This unfortunately has issues for accessibility (which was the whole point of using <desc> in the first place), so we can't just say "validation be damned" this time.

In theory, we could just display: none the <desc> elements themselves (since they're supposed to be invisible anyway), but if you remember from the whole HTML5shiv thing, old IE will take elements it doesn't recognize, like this:

  <foo> text </foo>

And turn it into this:

  <foo></foo> text </foo><//foo>

Yes, it's thinking you forgot to close your elements with tag names of foo and, incredibly, /foo. It boggles the mind.

So we have two real options here. The first is IE conditional comments:

  <!--[if gt IE 8]>
  <desc> [...] </desc>
<![endif]-->
<!--[if !IE]> -->
  <desc> [...] </desc>
<!-- <![endif]-->

Which is ugly, terrible, requires duplicating the content, and functional. But only in IE. Maybe babystting IE8 is the deepest your browser support goes, and this is all you need. You'll also have to use it around <text> elements, keep in mind.

You could also use a hack with xmlns but you'll have to serve as XHTML. AS IF YOU WOULD.

The second real option, and what I've resorted to, is wrapping the interior of each <desc> and <text> with a root <a>, which shares an HTML and an SVG tag name:

  <text>
  <a class="hide">
    [...]
  </a>
</text>

And then styling it out of existence:

  .hide {
  display: block;
  height: 0;
  width: 0;
  overflow: hidden;
}

EDIT: Whoops! You can't have any elements inside <desc> at all. Instead, do this:
  <desc>
<![CDATA[Your descriptive text here]]>
</desc>

And make sure you don't ampersand-escape anything between the CDATA delimiters.

This isn't ideal, but it works for all old browsers, not just misbehaving old IE. Using <a> without a href is actually valid, and shouldn't show up as a link to Assistive Technology. (Really old HTML used <a name="whatever"> to allow linking to parts of a page, exactly like id today, so <a> without href has been with us forever.) It seems to work in SVG fine, too, and there's always role="presentation" if need be.

I messed around with some text- and font- CSS properties, but either they work in both SVG and HTML (therefore hiding the text in both), or the ones that don't work in SVG (like text-indent) are being considered for inclusion in SVG2, so it's not very future-proof.

What about stuff inside <foreignObject>?

As far as I can tell, there's no hope. Only JavaScript will save you.