SVGFlow
Open editor
guides / svg-to-svelte

SVG to Svelte: $restProps, runes and dynamic fills

Svelte sits at the opposite pole from Vue: the markup needs no rewriting at all, and absolutely nothing is forwarded for you. A Svelte component that renders an svg and ignores what the caller passes is the default; making it behave like an element takes exactly one spread — written one way in Svelte 4 and another in Svelte 5's runes mode.

1. The markup survives untouched

Svelte components are HTML files with extra powers, so SVG attributes stay exactly as the exporter wrote them — stroke-width, fill-rule, clip-path, all kebab-case, all legal. The component's name is the file's name; nothing inside the markup carries it. The SVG to Svelte tool therefore changes only one thing about your file: it adds the spread that the next section is about.

2. Nothing falls through until you spread

Pass class or aria-hidden to a bare Svelte component and it lands nowhere — Svelte drops attributes the component does not handle. The fix is declared once, on the root:

<!-- Icon.svelte -->
<svg viewBox="0 0 24 24" fill="currentColor" {...$restProps}></svg>

<!-- caller -->
<Icon class="nav__icon" aria-hidden="true" />

Position is meaning: the spread comes after the file's own attributes, so anything the caller passes wins a clash — pass fill="none" and the file's currentColor loses, which is what a caller expects. Unlike Vue, nothing merges: a caller class replaces the file's class rather than joining it, so keep structural styling off the root or accept the override.

Try this in the editor
Load the sample icon, select a path, and change its fill — the source panel updates as you go.

3. The same component in Svelte 5 runes

$restProps works in Svelte 3, 4 and 5's legacy mode. A component written for runes mode declares its inputs with $props() and collects the remainder explicitly:

<script>
  let { accent = '#2f6df6', ...rest } = $props();
</script>

<svg viewBox="0 0 24 24" {...rest}>
  <path fill={accent} d="…" />
</svg>

Same behaviour, different declaration: named things are consumed as props, the rest rides through to the root. TypeScript is lang="ts" on the script tag plus a type on the destructure — no separate interface ceremony required.

4. Dynamic fills, in the same order as everywhere

The hierarchy does not change because the framework did: first currentColor, which needs no prop and follows the CSS colour of the container (the colour guide explains why that beats every alternative); then a single accent prop for a genuinely independent second tone — export let accent = '#2f6df6' in Svelte 4, the $props() default above in runes mode. Convert a file's hard-coded fills to currentColor with the Color Replacer before converting the file itself — the order matters, because afterwards the fills are code you would be editing by hand.

5. Gradient ids are still document-global

Two instances of an icon that defines id="g1" still fight over one definition — components do not scope SVG ids, in Svelte or anywhere. Strip the ids nothing references with the Optimizer; for defs the file actually uses, derive a per-instance id ($props.id() in Svelte 5, or a module-level counter in 4) and reference it as url(#${id}). The full anatomy of the collision — two icons, one gradient, both wrong — is in the React guide and applies unchanged.