Sitemap
Angela Dimovska

Angela Dimovska

How to Dynamically Modify SVGs in Jetpack Compose Using Coil

(and Why You’ll Never Need 100 Color Variants Again)

4 min readNov 3, 2025

--

Press enter or click to view image in full size

If you’ve ever worked on a feature that relies on SVG assets coming from the backend, you probably know the pain:

Sometimes, the backend provides a single SVG and expects the app to apply colors or tweaks dynamically instead of sending hundreds of variants of the same file.

This is great for efficiency, but tricky for us on the Android side. SVGs aren’t easily editable once loaded — at least, not without converting them into something else first.

Fortunately, with Coil, Jetpack Compose, and a bit of smart decoding, we can modify SVGs on the fly — whether that means recoloring elements, replacing text, changing stroke widths, or even applying themes dynamically.

Let’s go through how to make that happen.

The Core Idea

We’ll load the SVG through Coil, intercept it before rendering, and modify its raw XML content in memory before it ever reaches the screen.

That gives us the ability to:

  • Replace placeholder colors
  • Inject dynamic text or themes
  • Swap gradient definitions
  • Adjust stroke widths or opacity
  • Even hide or show SVG elements conditionally

In other words — Coil becomes a lightweight SVG preprocessor.

Example: Color Placeholders

Let’s start with a simple and practical use case — coloring.

Let’s imagine that our backend gives us a single SVG with placeholders instead of hardcoded colors:

<path fill="var(--placeholder-color-1, #FF0000)" d="some-path"/>  
<path fill="var(--placeholder-color-2, #00FF00)" d="some-path-2"/>
<path fill="var(--placeholder-color-3, #0000FF)" d="some-path-3"/>

The var( — placeholder-color-X, fallback) syntax lets us identify which part of the SVG we want to modify, without changing the backend asset itself.

Now, our job when receiving this SVG is to apply some colors before showing the image to the user.

How do we achieve this in the easiest way possible?

Building the Custom Decoder

We can start by extending Coil’s decoding pipeline by creating a custom Decoder that can read and mutate the SVG text before it’s drawn.

class ModifySvgDecoder(
private val result: SourceFetchResult,
private val options: Options
) : Decoder {

override suspend fun decode(): DecodeResult? {
val originalSvg = result.source.source().use { it.readUtf8() }

val isSvg = result.mimeType?.contains("svg", true) == true ||
originalSvg.contains("<svg", true)
if (!isSvg) return null

val colors: List<String> = options.extras.getOrDefault(ColorSVGExtras.colors)
val pattern: String = options.extras.getOrDefault(ColorSVGExtras.regex)
val regex = Regex(pattern)

val mutated = recolorSvg(originalSvg, colors, regex)

val mutatedSource = ImageSource(
source = okio.Buffer().writeUtf8(mutated),
fileSystem = options.fileSystem
)

return SvgDecoder(mutatedSource, options).decode()
}

class Factory : Decoder.Factory {
override fun create(result: SourceFetchResult, options: Options, loader: ImageLoader) =
ModifySvgDecoder(result, options)
}
}

The Regex Magic

We’ll detect placeholders inside the SVG using this regex:

private const val PLACEHOLDER_REGEX =
"""var\(--placeholder-color-(\d+)(?:,\s*(#[0-9a-fA-F]{3,8}))?\)"""

This matches things like var( — placeholder-color-1, #FF0000) and captures both the placeholder number (1) and fallback color (#FF0000).

Make sure you use the correct regex for your specific case.

Get Angela Dimovska’s stories in your inbox

Join Medium for free to get updates from this writer.

Then we replace each match with the correct runtime color:

private fun recolorSvg(svg: String, colors: List<String>, regex: Regex): String =
regex.replace(svg) { match ->
val index = (match.groupValues[1].toIntOrNull() ?: 1) - 1
colors.getOrNull(index)?.trim().orEmpty()
.ifEmpty { match.groupValues.getOrNull(2).orEmpty() }
}

Loading It with Coil

Now that we have set up the decoder, it is time to register it with Coil’s image loader:

val imageLoader = ImageLoader.Builder(context)
.components {
add(ModifySvgDecoder.Factory())
add(SvgDecoder.Factory())
}
.build()

And then, whenever we want to load a dynamic SVG, we make sure to send the colors and the corresponding regex.

val request = ImageRequest.Builder(context)
.data(svgUrl)
.apply {
extras[ColorSVGExtras.colors] = colorList
extras[ColorSVGExtras.regex] = PLACEHOLDER_REGEX
}
.memoryCacheKeyExtra(ColorSVGExtras.COLORS_KEY, colorList.joinToString(","))
.build()

And finally display the SVG.

AsyncImage(
model = request,
imageLoader = imageLoader,
contentDescription = null,
contentScale = ContentScale.Fit
)

Just make sure that the colors you’re sending are Android friendly. 😉

Going Beyond Colors

This decoder doesn’t have to stop at color replacements.

Because we intercept the raw SVG XML, we can modify almost anything.

For example:

  • Dynamic branding: Replace a <text> node’s content with the user’s name or team.
  • Theme-aware icons: Swap dark/light gradients dynamically based on isSystemInDarkTheme().
  • Localization: Inject translated labels directly into the SVG.
  • Data-driven graphics: Adjust chart lines, circle sizes, or opacities from your Compose state.

Why This Matters

Backend teams often don’t want to send 50 color variants or 10 theme versions of the same asset.

Instead, they can provide one clean, parameterized SVG, and we can handle all the customization client-side.

That saves:

  • Storage and CDN space
  • Network overhead
  • Development time
  • Mental overhead for maintaining duplicates

It also lets you build dynamic, data-driven visuals without leaving the vector world.

Wrapping Up

We often think of Coil as “just” an image loader, but with custom decoders, it becomes a content transformer.

With this approach, you can:

  • Modify SVG colors, labels, or gradients
  • Build dynamic visuals without duplicating assets
  • Keep backend and design pipelines simpler
  • Integrate seamlessly with Jetpack Compose

The next time your backend sends a single SVG and asks, “Can you color this one dynamically on your end?”

You can just smile and say: Already done.

You can find the full working demo here:

GitHub: Dynamically Colored SVG with Coil

--

--