
All icons from a popular open-source icon set exposed as ImageVector vectors for Compose; generated per upstream release, lazy-built on first use, with automated visual regression checks.
Every Tabler icon, outline and filled, as a Compose
Multiplatform ImageVector. They're generated from each Tabler release and
published within a few days of it.
import io.kontour.icons.tabler.Tabler
import io.kontour.icons.tabler.outline.Abc
import io.kontour.icons.tabler.filled.Star
Icon(Tabler.Outline.Abc, contentDescription = null)
Icon(Tabler.Filled.Star, contentDescription = null)The API is composables' compose-icons
API under a different package, so switching over only changes imports.
Targets: Android, JVM (desktop), iOS (iosArm64, iosSimulatorArm64), JS and
Wasm, the same six as Kontour UI.
From Kontour's own Maven repository, https://maven.kontour.io, with no token:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
exclusiveContent {
forRepository { maven("https://maven.kontour.io") }
filter { includeGroup("io.kontour") }
}
}
}exclusiveContent makes it the only place Gradle looks for io.kontour, and
asks it for nothing else. 3.49.0.1 is on Maven Central as well; earlier versions
are only on GitHub Packages, which needs a token even to read.
# gradle/libs.versions.toml
[versions]
tabler-icons = "3.49.0.1"
[libraries]
tabler-icons-outline = { module = "io.kontour:tabler-icons-outline", version.ref = "tabler-icons" }
tabler-icons-filled = { module = "io.kontour:tabler-icons-filled", version.ref = "tabler-icons" }| Artifact | What's in it |
|---|---|
io.kontour:tabler-icons-outline |
Tabler.Outline.*: every icon, about 5,200 of them |
io.kontour:tabler-icons-filled |
Tabler.Filled.*: the 1,050-odd icons that have a filled version |
io.kontour:tabler-icons-core |
Just the Tabler, Tabler.Outline and Tabler.Filled objects. Both sets bring it in |
The version is Tabler's. 3.48.0 holds the icons of Tabler 3.48.0. A
fourth part (3.48.0.1) is a re-release of the same icons with a fix to how
they're generated.
Each set depends on org.jetbrains.compose.ui:ui and nothing else. There's no
Compose compiler plugin to match, because none of the icons is a composable.
An icon's name is Tabler's name in PascalCase: arrow-big-up-line is
Tabler.Outline.ArrowBigUpLine, and a-b-2 is Tabler.Outline.AB2. Filled
icons have the same names in their own package, so use an import alias where a
file needs both:
import io.kontour.icons.tabler.filled.Star as FilledStar
import io.kontour.icons.tabler.outline.StarEach vector is built the first time it's read and kept, so an app only pays for the icons it draws.
com.composables:icons-tabler-outline-cmp and
com.composables:icons-tabler-filled-cmp with the two artifacts above.com.composables.icons.tabler with io.kontour.icons.tabler in
every import.The names, the receivers and the packages under them (.outline, .filled)
are the same. Tabler has renamed and removed some icons since the release
composables last generated from, and those imports fail to compile until
they're updated. tabler.io/icons has the current names.
release.yml runs on Monday mornings (UTC)
and asks npm for Tabler's latest release. If that release is already tagged here,
the job ends after about ten seconds. If it isn't, the workflow does this:
gradle.properties: tabler.version, and
tabler.integrity, npm's checksum of the tarball. Commits that and tags it
v<version>, locally for now.visual-check artifact.main as github-actions[bot], along with the tag.maven.kontour.io, an R2
bucket served in public, refusing a version that is already there. They
resolve as soon as the upload finishes.Unit tests run in ci.yml when the code that
generates the icons changes, not on every Tabler release. The generator fails
the build on anything in Tabler's data it doesn't understand, so a format change
stops a release instead of shipping broken icons.
A week with nothing new costs one ten-second check. A release costs one build, and a set Tabler left alone comes out of the Gradle build cache instead of being compiled again. The visual check adds a minute or two when it only checks the new icons, and six to eight minutes when the code changed and it checks all of them.
By hand: Actions → Release → Run workflow. Leave the version blank for the latest, or pick one. Tick dry run to build without committing, tagging or uploading anything.
A generator fix, re-released against the same icons: merge it, then
push a tag with a fourth part, git tag v3.48.0.1 && git push origin v3.48.0.1.
The build refuses a tag that doesn't match tabler.version.
If main is protected, the bot can't push its pin. Either let GitHub
Actions bypass the rule, or give the checkout in release.yml a deploy key
that's allowed to push.
./gradlew :buildSrc:test # the generator
./gradlew :tabler-icons-outline:generateTablerIcons # outline/build/generated/tabler/kotlin
./gradlew :tabler-icons-outline:jvmTest :tabler-icons-filled:jvmTest # every icon against Compose's parser
./gradlew publishToMavenLocal # all six targets; needs an Android SDKThe generator is in buildSrc/src/main/kotlin/tabler/:
GenerateTablerIcons downloads and verifies the npm tarball.Icon reads Tabler's normalised tabler-nodes-<set>.json.PathData parses SVG path data into PathBuilder calls.IconFile writes each file with KotlinPoet.Nothing generated is committed. The icons are built from the pinned release
every time, into build/.
visual-check/run.sh # every icon, about five minutes
visual-check/run.sh icons.txt # only these: one `outline/bell` or `filled/star` per line
# needs Node with Playwright: npm i -g playwright && npx playwright install chromiumThis draws each icon twice: Tabler's SVG in Chromium, and our ImageVector in
Compose Desktop. Then it compares the pixels.
An icon fails if more than 0.2% of its pixels are wrong. A pixel counts as wrong when it's more than 25% off every pixel within one pixel of it in the other drawing. That tolerance absorbs edges that two renderers place half a pixel apart, but a spike or a missing cap is several pixels long, so it still counts. The check also fails if a deliberately wrong drawing (a different icon, or a thinner stroke) gets through.
It writes a score for every icon to visual-check/build/report.tsv and a
contact sheet to visual-check/build/contact-sheet.png.
Every release runs it, as described above. It doesn't run on ordinary pushes. For a full check of a branch, go to Actions → CI → Run workflow and tick visual.
It's how we found a bug in Compose Multiplatform. On desktop, iOS and the web,
Compose's Skia renderer ignores StrokeJoin.Round and draws a miter join
instead, so the corners of 297 outline icons came out as spikes. Composables'
icons have the same problem. To work around it, each icon with a stroke
starts with one path that draws nothing and uses a miter join. After that, the
round joins reach Skia. IconFile explains how. Android's renderer doesn't have
the bug, and the extra path costs nothing there.
The icons are Tabler's, under the MIT licence: see LICENSE.
Every Tabler icon, outline and filled, as a Compose
Multiplatform ImageVector. They're generated from each Tabler release and
published within a few days of it.
import io.kontour.icons.tabler.Tabler
import io.kontour.icons.tabler.outline.Abc
import io.kontour.icons.tabler.filled.Star
Icon(Tabler.Outline.Abc, contentDescription = null)
Icon(Tabler.Filled.Star, contentDescription = null)The API is composables' compose-icons
API under a different package, so switching over only changes imports.
Targets: Android, JVM (desktop), iOS (iosArm64, iosSimulatorArm64), JS and
Wasm, the same six as Kontour UI.
From Kontour's own Maven repository, https://maven.kontour.io, with no token:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
exclusiveContent {
forRepository { maven("https://maven.kontour.io") }
filter { includeGroup("io.kontour") }
}
}
}exclusiveContent makes it the only place Gradle looks for io.kontour, and
asks it for nothing else. 3.49.0.1 is on Maven Central as well; earlier versions
are only on GitHub Packages, which needs a token even to read.
# gradle/libs.versions.toml
[versions]
tabler-icons = "3.49.0.1"
[libraries]
tabler-icons-outline = { module = "io.kontour:tabler-icons-outline", version.ref = "tabler-icons" }
tabler-icons-filled = { module = "io.kontour:tabler-icons-filled", version.ref = "tabler-icons" }| Artifact | What's in it |
|---|---|
io.kontour:tabler-icons-outline |
Tabler.Outline.*: every icon, about 5,200 of them |
io.kontour:tabler-icons-filled |
Tabler.Filled.*: the 1,050-odd icons that have a filled version |
io.kontour:tabler-icons-core |
Just the Tabler, Tabler.Outline and Tabler.Filled objects. Both sets bring it in |
The version is Tabler's. 3.48.0 holds the icons of Tabler 3.48.0. A
fourth part (3.48.0.1) is a re-release of the same icons with a fix to how
they're generated.
Each set depends on org.jetbrains.compose.ui:ui and nothing else. There's no
Compose compiler plugin to match, because none of the icons is a composable.
An icon's name is Tabler's name in PascalCase: arrow-big-up-line is
Tabler.Outline.ArrowBigUpLine, and a-b-2 is Tabler.Outline.AB2. Filled
icons have the same names in their own package, so use an import alias where a
file needs both:
import io.kontour.icons.tabler.filled.Star as FilledStar
import io.kontour.icons.tabler.outline.StarEach vector is built the first time it's read and kept, so an app only pays for the icons it draws.
com.composables:icons-tabler-outline-cmp and
com.composables:icons-tabler-filled-cmp with the two artifacts above.com.composables.icons.tabler with io.kontour.icons.tabler in
every import.The names, the receivers and the packages under them (.outline, .filled)
are the same. Tabler has renamed and removed some icons since the release
composables last generated from, and those imports fail to compile until
they're updated. tabler.io/icons has the current names.
release.yml runs on Monday mornings (UTC)
and asks npm for Tabler's latest release. If that release is already tagged here,
the job ends after about ten seconds. If it isn't, the workflow does this:
gradle.properties: tabler.version, and
tabler.integrity, npm's checksum of the tarball. Commits that and tags it
v<version>, locally for now.visual-check artifact.main as github-actions[bot], along with the tag.maven.kontour.io, an R2
bucket served in public, refusing a version that is already there. They
resolve as soon as the upload finishes.Unit tests run in ci.yml when the code that
generates the icons changes, not on every Tabler release. The generator fails
the build on anything in Tabler's data it doesn't understand, so a format change
stops a release instead of shipping broken icons.
A week with nothing new costs one ten-second check. A release costs one build, and a set Tabler left alone comes out of the Gradle build cache instead of being compiled again. The visual check adds a minute or two when it only checks the new icons, and six to eight minutes when the code changed and it checks all of them.
By hand: Actions → Release → Run workflow. Leave the version blank for the latest, or pick one. Tick dry run to build without committing, tagging or uploading anything.
A generator fix, re-released against the same icons: merge it, then
push a tag with a fourth part, git tag v3.48.0.1 && git push origin v3.48.0.1.
The build refuses a tag that doesn't match tabler.version.
If main is protected, the bot can't push its pin. Either let GitHub
Actions bypass the rule, or give the checkout in release.yml a deploy key
that's allowed to push.
./gradlew :buildSrc:test # the generator
./gradlew :tabler-icons-outline:generateTablerIcons # outline/build/generated/tabler/kotlin
./gradlew :tabler-icons-outline:jvmTest :tabler-icons-filled:jvmTest # every icon against Compose's parser
./gradlew publishToMavenLocal # all six targets; needs an Android SDKThe generator is in buildSrc/src/main/kotlin/tabler/:
GenerateTablerIcons downloads and verifies the npm tarball.Icon reads Tabler's normalised tabler-nodes-<set>.json.PathData parses SVG path data into PathBuilder calls.IconFile writes each file with KotlinPoet.Nothing generated is committed. The icons are built from the pinned release
every time, into build/.
visual-check/run.sh # every icon, about five minutes
visual-check/run.sh icons.txt # only these: one `outline/bell` or `filled/star` per line
# needs Node with Playwright: npm i -g playwright && npx playwright install chromiumThis draws each icon twice: Tabler's SVG in Chromium, and our ImageVector in
Compose Desktop. Then it compares the pixels.
An icon fails if more than 0.2% of its pixels are wrong. A pixel counts as wrong when it's more than 25% off every pixel within one pixel of it in the other drawing. That tolerance absorbs edges that two renderers place half a pixel apart, but a spike or a missing cap is several pixels long, so it still counts. The check also fails if a deliberately wrong drawing (a different icon, or a thinner stroke) gets through.
It writes a score for every icon to visual-check/build/report.tsv and a
contact sheet to visual-check/build/contact-sheet.png.
Every release runs it, as described above. It doesn't run on ordinary pushes. For a full check of a branch, go to Actions → CI → Run workflow and tick visual.
It's how we found a bug in Compose Multiplatform. On desktop, iOS and the web,
Compose's Skia renderer ignores StrokeJoin.Round and draws a miter join
instead, so the corners of 297 outline icons came out as spikes. Composables'
icons have the same problem. To work around it, each icon with a stroke
starts with one path that draws nothing and uses a miter join. After that, the
round joins reach Skia. IconFile explains how. Android's renderer doesn't have
the bug, and the extra path costs nothing there.
The icons are Tabler's, under the MIT licence: see LICENSE.