Skip to content
Blog

SVG Rendering on Flutter Web: flutter_svg in the Browser

How flutter_svg 2.3.4 renders vectors into Flutter's web canvas, serving precompiled .svg.vec assets, raster vs picture strategy, and when a real DOM <svg> is the better call.

Published on • November 13, 2026

AI Assistant

flutter-svg-rendering covers SvgPicture on mobile — loading, tinting, precaching, and precompiling with vector_graphics_compiler. This post asks the web-specific question: what actually happens to that SVG in a browser, and what should you do differently? The short version: flutter_svg (2.3.4, a Flutter Favorite with web listed among its platforms) is pure Dart, so it works identically on web — but “identically” has consequences that only exist in the browser.

No DOM, no <svg> element — that’s the whole story

Flutter web renders your widget tree into a canvas (CanvasKit/Skwasm via WebGL/WebGPU), not into HTML elements. When SvgPicture parses your file, the output is a Picture that gets painted into that canvas — the page’s DOM never contains an <svg> node. Everything follows from that:

  • Page CSS can’t touch it. No svg path { fill: … } selectors, no CSS animations on parts of the graphic, no inherited currentColor from your web stylesheet. Theming happens through Flutter APIs — colorFilter, ColorMapper.substitute — exactly as on mobile.
  • Nothing inside is selectable, searchable, or indexable. Text rendered from SVG paths is invisible to Ctrl+F and to crawlers. Fine for logos and illustrations; disqualifying for an infographic whose text should be found.
  • Accessibility goes through semantics, not the DOM. Always pass semanticsLabel — on web it lands in Flutter’s semantics tree for screen readers, the only channel available since there’s no real element to label.

Ship .svg.vec, not the raw file

The parse cost of raw SVG happens on the main isolate on every cold start (picture-mode results are cached in memory afterward, but not across reloads). The fix is the same as mobile, with an extra web benefit: the compiled artifact is a small binary that’s cheap to parse and cacheable as a static asset:

dart run vector_graphics_compiler -i assets/logo.svg -o assets/logo.svg.vec
import 'package:vector_graphics/vector_graphics.dart';

SvgPicture(const AssetBytesLoader('assets/logo.vec'));

On web, that file goes out over HTTP like any asset — give it long-lived caching headers (Cache-Control: public, max-age=31536000, immutable if the filename is content-hashed) and it’s a one-time download per deploy. SvgPicture.network(...) works too, but mind CORS: the canvas-tainting rules mean a cross-origin SVG fetched without Access-Control-Allow-Origin can fail or degrade — bundling the .vec as a first-party asset sidesteps the entire class of problem.

Picture vs raster strategy on the web

flutter_svg’s render strategy choice matters more under a browser’s frame budget:

  • picture (default): the vector is re-tessellated at paint time — full fidelity at any zoom, slightly more per-frame work.
  • raster: the SVG is rendered once into an Image at the current resolution and drawn thereafter — cheaper per frame, but resolution-scaling flexibility is sacrificed (a browser window resize or DPR change can leave it soft until re-rendered).
SvgPicture(
  const AssetBytesLoader('assets/hero.vec'),
  renderStrategy: RenderStrategy.raster, // static art that never zooms
  width: 480,
  height: 270,
);

For static decorative graphics that sit still on screen, raster is often the right web default; for anything user-zoomable (maps, diagrams with pinch), stay with picture. This mirrors the wider canvas trade-off — Impeller/Skwasm are good at vectors, but “good” isn’t “free” at 60fps with fifty logos on screen.

Inline strings: sprites and per-instance icons

If you ship one sprite sheet of icons, SvgPicture.string parses from memory and skips the asset pipeline entirely — useful on web when the SVG already lives in a Dart constant or arrives with an API payload:

const iconBolt = '''
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z" fill="currentColor"/>
</svg>
''';

Widget boltIcon(Color tint) => SvgPicture.string(
      iconBolt,
      colorFilter: ColorFilter.mode(tint, BlendMode.srcIn),
      semanticsLabel: 'Bolt',
    );

Two web-specific cautions: a string parsed on every rebuild is re-parsed work (hoist it to a const or precache with vg.loadPicture), and currentColor inside the string resolves against Flutter’s paint color — not your page’s CSS color — so pass the tint explicitly.

When a real DOM <svg> wins

If the graphic needs text selection, CSS theming, SEO, or crispness independent of Flutter’s canvas — or if it lives outside the Flutter app on a marketing page — use native SVG. Two integration paths exist: embed HTML with HtmlElementView/dart:ui platform views (correct but heavyweight — it punches a DOM hole through your canvas), or use a DOM-first Dart framework like Jaspr for those pages, which the Flutter docs point to for text-rich document-centric content. A pragmatic split many teams land on: Flutter app + flutter_svg for in-app art, plain HTML/CSS/SVG for the landing pages around it.

Sizing, DPR, and the soft-image trap

The browser’s devicePixelRatio compounds with the raster strategy. A raster-rendered SVG laid out at 200 logical pixels on a 2x display must raster at 400 physical pixels or it renders soft — and unlike a PNG, users expect vectors to stay sharp at any zoom. Practical rules:

  • Give SvgPicture explicit width/height (or a tight parent constraint) so the raster target is deterministic; an unconstrained picture in a scroll view has no defined pixel size to raster into.
  • Re-check after OS display-scale changes and browser zoom — MediaQuery.devicePixelRatio shifts are when cached rasters go stale.
  • For print, PDF export, or pinch-zoom galleries, picture mode is mandatory; the raster cache is a screen-resolution artifact.

Testing what renders

A widget test can assert that a picture resolved without throwing (parse errors surface as console logs, not exceptions — the README is explicit that there’s no visual error state):

testWidgets('hero svg loads from vec asset', (tester) async {
  await tester.pumpWidget(const MaterialApp(
    home: Scaffold(body: SvgPicture(AssetBytesLoader('assets/hero.vec'))),
  ));
  await tester.pumpAndSettle();
  expect(find.byType(SvgPicture), findsOneWidget);
  expect(tester.takeException(), isNull);
});

Pair it with one manual pass in DevTools: confirm the .vec request returned 200 with a Cache-Control header rather than being swallowed by an SPA rewrite — the failure mode covered in flutter-web-hosting-rewrites-url-strategy.

The web checklist

  1. Precompile to .svg.vec and serve it first-party with immutable caching.
  2. Pass semanticsLabel on every picture — it’s the only a11y channel on canvas.
  3. Prefer colorFilter/ColorMapper over per-file recolors; the DOM can’t help you here.
  4. Choose raster for static decoration, picture for zoomable or resize-sensitive art.
  5. Give explicit dimensions so raster targets are deterministic under DPR changes.
  6. Hoist SvgPicture.string sources to constants; parse once, not per frame.
  7. Verify at browser zoom 200% and a changed devicePixelRatio — that’s where raster-strategy softness shows.
  8. Keep real HTML <svg> for anything whose text must be selectable or indexable.

Takeaway: flutter_svg on web paints into Flutter’s canvas rather than emitting a DOM <svg> — bundle precompiled .vec assets with immutable cache headers, theme through ColorFilter instead of CSS, label everything for semantics, and keep a real HTML <svg> (or Jaspr page) wherever text selection, SEO, or CSS styling matters.

Sources