During the development Jetpack Compose / Compose Multiplatform, we often faced the challenge of converting icons from SVG or XML format to ImageVector. While there are existing tools available for this purpose, we found that they often fell short in terms of usability, reliability, and the quality of the generated code and in some cases, even being paid 😄.
To address these issues, we decided to create our own tool that would streamline the conversion process and provide a better user experience.
The primary goal of this project is to offer a fast, reliable, and user-friendly solution for converting SVG and XML icons to ImageVector format, while also allowing for customization of the generated code to meet individual project needs.
Note
This project is especially relevant now as Material Icons is no longer maintained and not recommended for use in your apps. Learn more.
- 🔌 IntelliJ IDEA / Android Studio plugin
- 🖥️ CLI tool
- 🐘 Gradle plugin
- Web app (🚧 under development 🚧)
- Support conversion from SVG and XML
- Custom kotlinpoet generator with streamlined code formatting:
- code alignment and formatting
- remove redundant code by default (e.g.
publickeyword) - remove unused imports (e.g.
kotlin.*package) - skip default ImageVector parameters
- support generation as backing property or lazy property
- optional trailing comma and explicit mode
- customize code indent
- Ability to create your unique project icon pack (+nested packs if necessary)
- High performance (6k icons processing ~5sec)
- Two conversion modes: Simple and IconPack
- Support for Drag&Drop files/directories and pasting content from clipboard
- Easy option to add more icons into existing project icon pack
- Export generated ImageVector to clipboard or file (depends on the mode)
- Fully customizable setting for generated icons
- Build-in ImageVector previewer for any icons without compilation ✨
- The plugin is completely built using Compose Multiplatform and Tiamat navigation library
More exclusive features under development, stay tuned 🌚
Note
One-click solution to convert SVG/XML to ImageVector (requires only specifying the package).
- Rename icon
- Preview current ImageVector
- Copy generated ImageVector to clipboard
Demo:
simple_mode_demo.mp4
Note
Facilitates creating an organized icon pack with extension properties for your pack object, previewing the list of
icons, and batch exporting them to your specified directory.
Demo:
iconpack_mode_new_demo.mp4
Note
Instead of importing icon pack settings, the plugin provides a direct way to import an already created icon pack from a Kotlin file.
Important
Editing features are limited for now; you can only load an existing pack and add more nested packs.
Demo:
iconpack_mode_existing_demo.mp4
We personally find it very useful to have a previewer for ImageVector (such we have for SVG or XML). Previewer available for any ImageVector formats (backing or lazy property, legacy google material icons) without compose @Preview annotation and project compilation.
Previewer actions:
- Change icon background (pixel grid, white, black)
- Zoom in, zoom out icon without loosing quality
- Draw as actual size
- Fit icon to window
Demo:
imagevector_previewer.mp4
When IDEA auto-completion popup is shown for any ImageVector property, the preview image will be displayed in the popup.
Preview inside gutter available for any ImageVector property. By clicking on the gutter icon, the original file will be opened in the editor with embedded previewer.
| Plugin version | Min IntelliJ IDEA / Android Studio |
|---|---|
| 0.1.0 - 0.14.0 | IntelliJ IDEA 2024.1, Android Studio Koala |
| 0.15.0+ | IntelliJ IDEA 2024.2, Android Studio Ladybug |
-
Find plugin inside IDE:
Settings > Plugins > Marketplace > Search for "Valkyrie" > Install Plugin
-
Manually: Download the latest release or build your self and install it manually using Settings -> Plugins -> ⚙️ -> Install plugin from disk...
Precondition: IntelliJ IDEA with installed Plugin DevKit
Run ./gradlew buildPlugin to build plugin locally. Artifact will be available in
tools/idea-plugin/build/distributions/
folder
or run plugin in IDE using: ./gradlew runIde
CLI tools can be easily integrated into scripts and automated workflows, allowing you to convert icons from specific source with predefined settings.
brew install ComposeGears/repo/valkyrieDownload latest CLI tool from releases.
Unzip the downloaded archive and run the CLI tool from bin folder in the terminal
./valkyrieYou should see this message
A simple example of how to get the latest version of the CLI tool. It can be executed on CI/CD with predefined parameters.
#!/bin/bash
VERSION="cli-1.0.0"
TARGET_DIR="valkyrie-cli"
ASSET_NAME="tmp.zip"
LATEST_CLI_RELEASE_URL=$(curl --silent "https://api.github.com/repos/ComposeGears/Valkyrie/releases/tags/$VERSION" \
| jq -r '.assets[] | select(.name | startswith("valkyrie-cli")) | .browser_download_url')
curl -L -o "$ASSET_NAME" "$LATEST_CLI_RELEASE_URL"
mkdir -p "$TARGET_DIR"
unzip -o "$ASSET_NAME" -d "$TARGET_DIR"
rm "$ASSET_NAME"
cd "$TARGET_DIR/bin" || exit
./valkyrie --versionA part of the CLI tool that allows you to create an icon pack with nested packs.
Usage:
./valkyrie iconpack [<options>]
Demo:
cli_iconpack.mp4
A part of the CLI tool that allows you to convert SVG/XML files to ImageVector.
Usage:
./valkyrie svgxml2imagevector [<options>]
Demo:
cli_svgxml2imagevector.mp4
Additional command to display embedded CLI changelog
Usage:
./valkyrie changelogOutput example:
Run ./gradlew buildCLI to build minified version of CLI tool. Artifact will be available in
tools/cli/build/distributions/valkyrie-cli-*.**.*-SNAPSHOT.zip.
The Gradle plugin automates the conversion of SVG/XML files to Compose ImageVector format during the build process. It's ideal for projects that need to version control icon sources and generate type-safe Kotlin code automatically.
- Team collaboration: Keep SVG/XML sources in version control and let the build system generate Kotlin code for everyone
- CI/CD pipelines: Ensure icons are always generated consistently across different environments
- Design system integration: Automatically sync icon updates from design tools without manual conversion
- Large icon libraries: Efficiently manage hundreds or thousands of icons with minimal manual intervention
Define in your libs.versions.toml:
[plugins]
valkyrie = "io.github.composegears.valkyrie:latest-version"Add the plugin to your build.gradle.kts:
plugins {
alias(libs.plugins.valkyrie)
}Full gradle plugin API specification (see practical examples below):
valkyrie {
// Required: Package name for generated icons
// Defaults to Android 'namespace' if Android Gradle Plugin is applied
packageName = "com.example.app.icons"
// Optional: Custom output directory (default: build/generated/sources/valkyrie)
outputDirectory = layout.buildDirectory.dir("generated/valkyrie")
// Optional: Resource directory name containing icon files (default: "valkyrieResources")
// Icons will be discovered in src/{sourceSet}/{resourceDirectoryName}/
// Example: src/commonMain/valkyrieResources/, src/androidMain/valkyrieResources/
resourceDirectoryName = "valkyrieResources"
// Optional: Generate during IDE sync for better developer experience (default: false)
generateAtSync = false
// Optional: Force all generated ImageVectors to have a specific autoMirror value (default: not specified)
// When set to true, all icons will have autoMirror = true
// When set to false, all icons will have autoMirror = false
// When not specified, the autoMirror value from the original icon file will be preserved
// This can be overridden at the icon pack or nested pack level
autoMirror = false
// Optional: Code style configuration for generated code
codeStyle {
// Add explicit `public` modifier to generated declarations (default: false)
useExplicitMode = false
// Number of spaces used for each level of indentation in generated code (default: 4)
indentSize = 4
}
// Optional: ImageVector generation configuration
imageVector {
// Output format for generated ImageVectors (default: BackingProperty)
outputFormat = OutputFormat.BackingProperty // or OutputFormat.LazyProperty
// Use predefined Compose colors instead of hex color codes (e.g. Color.Black instead of Color(0xFF000000)) (default: true)
useComposeColors = true
// Generate `@Preview` function for ImageVector (default: false)
generatePreview = false
// Specifies the type of Preview annotation to generate for @Preview
previewAnnotationType = PreviewAnnotationType.AndroidX
// Insert a trailing comma after the last element of ImageVector.Builder block and path params (default: false)
addTrailingComma = false
}
// Optional icon pack object configuration
iconPack {
// Required: Name of the root icon pack object
name = "ValkyrieIcons"
// Required: Target source set for generated icon pack object
targetSourceSet = "commonMain"
// Optional: Generate flat package structure without subfolders (default: false)
useFlatPackage = false
// Optional: Force all ImageVectors in this icon pack to have a specific autoMirror value (default: not specified)
// When set, overrides the root level autoMirror setting
// This can be overridden at the nested pack level
autoMirror = true
// Optional: Nested icon packs configuration
nested {
// Required: Name of the nested icon pack object
name = "Outlined"
// Required: The source folder path containing icons for this nested pack, relative to the `resourceDirectoryName`.
sourceFolder = "outlined"
// Optional: Force all ImageVectors in this nested pack to have a specific autoMirror value (default: not specified)
// When set, overrides the icon pack level and root level autoMirror settings
autoMirror = false
}
// You can add more nested packs if necessary
}
}Place your icon files in the resources directory:
src/
└── commonMain/
└── valkyrieResources/
├── add.svg
├── delete.svg
└── ic_home.xml
The plugin automatically discovers icons from src/{sourceSet}/valkyrieResources/ in all source sets.
Run the Gradle task to generate ImageVector sources:
./gradlew generateValkyrieImageVectorSimply convert SVG/XML files to ImageVector in the specified package. For this example, we will use a multiplatform project structure.
plugins {
kotlin("multiplatform")
alias(libs.plugins.valkyrie)
}
valkyrie {
packageName = "com.example.app.icons"
}
// other codePlace icons in the valkyrieResources directory:
src/
└── commonMain/
└── valkyrieResources/
├── ic_brush.xml
├── ic_compose_color.xml
├── ic_linear_gradient.svg
├── ic_several_path.xml
└── ic_transparent_fill_color.xml
Run the Gradle task:
./gradlew generateValkyrieImageVectorObserve generated code in build directory:
Use icon from your Compose code:
@Composable
fun Demo() {
Image(
imageVector = ComposeColor,
contentDescription = "Color"
)
}For better organization and type-safe access to your icons, you can create an icon pack. For this example, we will use a multiplatform project structure.
plugins {
kotlin("multiplatform")
alias(libs.plugins.valkyrie)
}
valkyrie {
packageName = "com.example.app.icons"
iconPack {
name = "ValkyrieIcons"
targetSourceSet = "commonMain" // icon pack object will be generated in commonMain source set
}
}Place icons in the valkyrieResources directory:
src/
└── commonMain/
└── valkyrieResources/
├── ic_brush.xml
├── ic_compose_color.xml
├── ic_linear_gradient.svg
├── ic_several_path.xml
└── ic_transparent_fill_color.xml
Run the Gradle task:
./gradlew generateValkyrieImageVectorObserve generated code in build directory with icon pack object and icons:
Use icon from your Compose code:
@Composable
fun Demo() {
Image(
imageVector = ValkyrieIcons.LinearGradient,
contentDescription = null
)
}For larger projects with multiple icon sets, you can organize icons into a structured icon pack with nested packs. This approach provides better organization and type-safe access to your icons.
For this example, we will use a multiplatform project structure.
plugins {
kotlin("multiplatform")
alias(libs.plugins.valkyrie)
}
valkyrie {
packageName = "com.example.app.icons"
iconPack {
name = "ValkyrieIcons"
targetSourceSet = "commonMain"
nested {
name = "Outlined"
sourceFolder = "outlined"
}
nested {
name = "Filled"
sourceFolder = "filled"
}
}
}Organize your icons in nested folders:
src/
└── commonMain/
└── valkyrieResources/
├── outlined/
│ ├── add.svg
│ ├── delete.svg
│ └── settings.svg
└── filled/
├── home.svg
├── user.svg
└── search.svg
Run the Gradle task:
./gradlew generateValkyrieImageVectorObserve generated code in build directory with icon pack object and icons:
Use icons from your Compose code:
@Composable
fun Demo() {
Image(
imageVector = ValkyrieIcons.Outlined.Add,
contentDescription = "Add"
)
Image(
imageVector = ValkyrieIcons.Filled.Home,
contentDescription = "Home"
)
}To keep generated icons in your source folder (e.g. for version control), you can create custom Gradle tasks to sync the generated files after the generation task.
val copyTask = tasks.register<Copy>("copyValkyrieIcons") {
dependsOn(tasks.named("generateValkyrieImageVector"))
from(layout.buildDirectory.dir("generated/sources/valkyrie"))
into(layout.projectDirectory.dir("src"))
}
val cleanTask = tasks.register<Delete>("cleanValkyrieGenerated") {
delete(layout.buildDirectory.dir("generated/sources/valkyrie"))
}
tasks.register("updateValkyrieIcons") {
dependsOn(copyTask)
finalizedBy(cleanTask)
}The autoMirror parameter controls whether icons should automatically flip horizontally when used in right-to-left (
RTL) layouts. This is particularly useful for directional icons like arrows, chevrons, or navigation elements.
Configuration hierarchy:
The plugin supports a three-level hierarchy for autoMirror configuration:
- Root level - applies to all icons across the project
- Icon pack level - overrides root level for all icons in the pack
- Nested pack level - overrides both icon pack and root level for icons in the nested pack
For example, to force enable RTL support for icons in the Navigation nested pack:
valkyrie {
packageName = "com.example.app.icons"
iconPack {
name = "ValkyrieIcons"
targetSourceSet = "commonMain"
nested {
name = "Navigation"
sourceFolder = "navigation"
autoMirror = true
}
nested {
name = "Logos"
sourceFolder = "logos"
}
}
}In this example:
- Icons in the
Navigationnested pack will haveautoMirror = true - Icons in the
Logosnested pack (and any other nested pack without an explicitautoMirrorsetting) will preserve the originalautoMirrorvalue from the source icon file
| Backing property | Lazy property |
package io.github.composegears.valkyrie.backing.outlined
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import io.github.composegears.valkyrie.backing.BackingIcons
val BackingIcons.Outlined.Add: ImageVector
get() {
if (_Add != null) {
return _Add!!
}
_Add = ImageVector.Builder(
name = "Outlined.Add",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
).apply {
path(fill = SolidColor(Color(0xFF232F34))) {
moveTo(19f, 13f)
lineTo(13f, 13f)
lineTo(13f, 19f)
lineTo(11f, 19f)
lineTo(11f, 13f)
lineTo(5f, 13f)
lineTo(5f, 11f)
lineTo(11f, 11f)
lineTo(11f, 5f)
lineTo(13f, 5f)
lineTo(13f, 11f)
lineTo(19f, 11f)
lineTo(19f, 13f)
close()
}
}.build()
return _Add!!
}
@Suppress("ObjectPropertyName")
private var _Add: ImageVector? = null |
package io.github.composegears.valkyrie.lazy.outlined
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import io.github.composegears.valkyrie.lazy.LazyIcons
val LazyIcons.Outlined.Add: ImageVector by lazy(LazyThreadSafetyMode.NONE) {
ImageVector.Builder(
name = "Outlined.Add",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
).apply {
path(fill = SolidColor(Color(0xFF232F34))) {
moveTo(19f, 13f)
lineTo(13f, 13f)
lineTo(13f, 19f)
lineTo(11f, 19f)
lineTo(11f, 13f)
lineTo(5f, 13f)
lineTo(5f, 11f)
lineTo(11f, 11f)
lineTo(11f, 5f)
lineTo(13f, 5f)
lineTo(13f, 11f)
lineTo(19f, 11f)
lineTo(19f, 13f)
close()
}
}.build()
} |
Source SVG icon:
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e8eaed">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>ImageVector output:
| Valkyrie | composables.com |
package io.github.composegears.valkyrie
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val Add: ImageVector
get() {
if (_Add != null) {
return _Add!!
}
_Add = ImageVector.Builder(
name = "Add",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFFE8EAED))) {
moveTo(19f, 13f)
horizontalLineToRelative(-6f)
verticalLineToRelative(6f)
horizontalLineToRelative(-2f)
verticalLineToRelative(-6f)
horizontalLineTo(5f)
verticalLineToRelative(-2f)
horizontalLineToRelative(6f)
verticalLineTo(5f)
horizontalLineToRelative(2f)
verticalLineToRelative(6f)
horizontalLineToRelative(6f)
verticalLineToRelative(2f)
close()
}
}.build()
return _Add!!
}
@Suppress("ObjectPropertyName")
private var _Add: ImageVector? = null
|
import androidx.compose.runtime.Composable
import androidx.compose.foundation.Image
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
private var _Add: ImageVector? = null
public val Add: ImageVector
get() {
if (_Add != null) {
return _Add!!
}
_Add = ImageVector.Builder(
name = "Add",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(
fill = null,
fillAlpha = 1.0f,
stroke = null,
strokeAlpha = 1.0f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1.0f,
pathFillType = PathFillType.NonZero
) {
moveTo(0f, 0f)
horizontalLineToRelative(24f)
verticalLineToRelative(24f)
horizontalLineTo(0f)
verticalLineTo(0f)
close()
}
path(
fill = SolidColor(Color(0xFFE8EAED)),
fillAlpha = 1.0f,
stroke = null,
strokeAlpha = 1.0f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1.0f,
pathFillType = PathFillType.NonZero
) {
moveTo(19f, 13f)
horizontalLineToRelative(-6f)
verticalLineToRelative(6f)
horizontalLineToRelative(-2f)
verticalLineToRelative(-6f)
horizontalLineTo(5f)
verticalLineToRelative(-2f)
horizontalLineToRelative(6f)
verticalLineTo(5f)
horizontalLineToRelative(2f)
verticalLineToRelative(6f)
horizontalLineToRelative(6f)
verticalLineToRelative(2f)
close()
}
}.build()
return _Add!!
}
|
CLI options --iconpack-name and --nested-packs removed in favour of --iconpack
Single pack
❌ ./valkyrie --iconpack-name=ValkyrieIcons
✅ ./valkyrie --iconpack=ValkyrieIcons
Nested packs
❌ ./valkyrie --iconpack-name=ValkyrieIcons --nested-packs=Colored,Filled
✅ ./valkyrie --iconpack=ValkyrieIcons.Colored,ValkyrieIcons.Filled
Thank you for your contributions and support! ❤️
Developed by ComposeGears 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.










