
Hand-drawn, animated vector illustrations and empty-states drawn on canvas; asset-free, reskinnable palette, toggleable sketch or painted modes, subtle built-in animations.
Hand-drawn, animated illustrations & empty states for Compose Multiplatform.
Sketchy is a Kotlin Multiplatform library of sketch-style, line-art artwork —
the kind of warm, human illustration you'd want for onboarding flows and empty
states — drawn entirely on Canvas, animated out of the box, and fully
themeable to match your app. It runs on Android, iOS, desktop and the web from
one commonMain source set.
| Target | Status |
|---|---|
| Android | minSdk 26 |
| iOS |
iosArm64, iosSimulatorArm64
|
| Desktop (JVM) | Windows, macOS, Linux |
| Web |
wasmJs (browser) |
iosX64 (the Intel simulator) is not supported — Compose Multiplatform no
longer publishes artifacts for it.
https://github.com/user-attachments/assets/2db98caf-7201-4e06-97de-919f964268de
Most illustration libraries ship static SVGs or Lottie files — dependencies outside Compose's own rendering pipeline, hard to restyle, and rarely animated in a way that feels alive. Sketchy takes a different approach:
DrawScope, so it's crisp at any size and ships as pure Kotlin
— no drawables, no Lottie JSON, no binary bloat.animate = false when you want
a static frame instead.colorful = true and the same
scene is painted in full: shaded surfaces, contact shadows, highlights. No
second set of artwork, and never a background of its own either way.SketchyColors, Dp, String, TextStyle) — no XML
theming, no design-system lock-in. It doesn't even depend on Material.| Catalog | Count | Entry point |
|---|---|---|
| Featured illustrations | 6 elaborate full scenes | SketchyIllustration(modifier, sketch, animate, colorful, colors) |
| Onboarding illustrations | 24, across 9 categories | SketchyIllustration(modifier, sketch, animate, colorful, colors) |
| Empty states | 80, across 10 categories | SketchyEmptyState(state, modifier, animate, colorful, colors, illustrationSize, title, subtitle, titleStyle, subtitleStyle, spacing) |
| Icons | 20, across 1 category | SketchyIcon(icon, modifier, style, tint, animate) |
Every one of the 110 illustrations and empty states draws two ways from the same code — a
colourless hand-drawn outline by default, or fully painted with colorful = true. Icons are a
different, simpler model: monochrome by design (a single tint rather than the full palette), each
one rendering in five families off the same hand-drawn geometry — Default, Outlined, Sharp,
Filled and TwoTone.
A demo ships alongside the library as a live, searchable catalog of every
illustration and empty state — the fastest way to browse what's available and
copy the exact usage snippet for whatever you pick. Its UI lives in
:composeApp and is shared across all four platforms:
./gradlew :composeApp:run # desktop
./gradlew :androidApp:installDebug # Android
./gradlew :composeApp:wasmJsBrowserDevelopmentRun # web
open iosApp/iosApp.xcodeproj # iOS (run from Xcode)Sketchy never ships two sets of artwork. Each scene is written once, and the
colorful flag decides how the palette hands its colours back to the drawing
code:
colorful = false (default)
|
colorful = true |
|
|---|---|---|
| Look | Pure line-art — ink outlines, nothing filled in | Gradient-shaded surfaces, contact shadows, highlights, under the same outlines |
| Palette slots used |
ink, inkSoft, inkFaint + the four accents |
All of the above plus the light and material slots |
| Colour | Only small accent marks — a sparkle, a glint, one highlighted bar | Full colour throughout |
| Background | Transparent | Transparent |
| Best for | Minimal / monochrome UIs, dark mode, matching a single brand ink | Onboarding heroes, marketing screens, playful apps |
Neither mode ever paints a background of its own, so a scene drops onto whatever surface your screen already has.
How it works: when a scene is outlined, every surface, light and material slot
of the palette reads back as Color.Transparent, so the fill simply isn't
painted and what survives is the ink outline of the same shape. That's why
colorful is one boolean and not a second catalog.
Sketchy is live on Maven Central as io.github.ali0092:sketchy — the version
badge above always reflects the latest published release.
A multiplatform consumer adds it to commonMain — mavenCentral() is already
declared by default in a Kotlin Multiplatform project's repositories, no extra
repository setup required:
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.ali0092:sketchy:1.0.9")
}
}
}An Android-only consumer adds it the usual way:
// app/build.gradle.kts
dependencies {
implementation("io.github.ali0092:sketchy:1.0.9")
}Sketchy is also mirrored on JitPack, kept at the same version and coordinate as Maven Central, as a fallback in case Maven Central isn't reachable from your network:
dependencies {
implementation("io.github.ali0092:sketchy:1.0.9")
}with the JitPack repository declared in settings.gradle.kts:
dependencyResolutionManagement {
repositories {
mavenCentral()
maven("https://jitpack.io")
}
}import com.sketchy.library.illustrations.Sketch
import com.sketchy.library.illustrations.SketchyIllustration
SketchyIllustration(
sketch = Sketch.PlanTasks,
modifier = Modifier.size(280.dp)
)Every Sketch scales to whatever size you give it and animates on a loop by
default. Pass animate = false to freeze it on its resting frame — handy for
lower-power devices or a still hero image.
// Colourless — the default. Ink outlines, a few accent marks, nothing filled in.
SketchyIllustration(
sketch = Sketch.MorningCoffee,
modifier = Modifier.size(280.dp),
)
// Colourful — the same scene, painted: shaded surfaces, shadows, highlights.
SketchyIllustration(
sketch = Sketch.MorningCoffee,
modifier = Modifier.size(280.dp),
colorful = true,
)colorful works on every illustration and every empty state, and it's an
ordinary parameter — drive it from a setting, from isSystemInDarkTheme(), or
from a single app-wide constant:
SketchyEmptyState(
state = EmptyState.EmptyCart,
colorful = !isSystemInDarkTheme(),
)The six Featured scenes are the elaborate ones — a whole scene rather than a single motif, one source file each, built on the shading primitives the painted mode uses. They're the ones worth giving a full-bleed hero slot:
SketchyIllustration(
sketch = Sketch.ReadingNook, // or WarmWelcome, MorningCoffee, HomeWorkspace, GroceryRun, RainyWindow
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
colorful = true,
)They take exactly the same parameters as every other Sketch — animate,
colorful and colors all behave identically — and they render colourless
just as happily if that's your look. Because they carry the most detail, they
also gain the most from colorful = true, so they're the best place to start
if you're deciding between the two modes.
colors is a plain SketchyColors parameter — override any subset of it and
every stroke, fill, and sparkle in the scene repaints to match, no XML themes
or design-system lock-in required.
A colourless scene only ever draws from the first block of the palette — ink,
inkSoft, inkFaint and the four accents (accent, accentGreen,
accentBlue, accentRed). The light and material slots below them are what
the colourful version fills with, so reskinning a colourless app means changing
the ink and the accents and nothing else:
import com.sketchy.library.SketchyColors
import com.sketchy.library.illustrations.Sketch
import com.sketchy.library.illustrations.SketchyIllustration
SketchyIllustration(
sketch = Sketch.BuildBetterHabits,
modifier = Modifier.size(280.dp),
colors = SketchyColors(
ink = MaterialTheme.colorScheme.onSurface,
accent = MaterialTheme.colorScheme.primary,
accentBlue = MaterialTheme.colorScheme.secondary,
)
)If you use colorful = true, the rest of the palette is worth a look too —
it's grouped so you can retint a whole painted scene a few slots at a time:
| Block | Slots | What it paints |
|---|---|---|
| Ink |
ink, inkSoft, inkFaint
|
Every outline, in both modes |
| Accents |
accent, accentGreen, accentBlue, accentRed
|
Small coloured marks, in both modes |
| Light & atmosphere |
paper, sun, sunDeep, glow, sky, skyDeep, shade, shadeSoft
|
Key light, bloom, shadows — colourful only |
| Materials |
wood, woodDark, leaf, leafDark, terracotta, clay, fabric, fabricDark, metal, metalDark, skin, skinDark, hair, coffee
|
The surfaces themselves — colourful only |
Overriding a material slot while a scene is colourless is harmless — it's simply never read.
import com.sketchy.library.SketchyColors
import com.sketchy.library.emptystates.EmptyState
import com.sketchy.library.emptystates.SketchyEmptyState
SketchyEmptyState(
state = EmptyState.NoInternet,
colors = SketchyColors(
ink = MaterialTheme.colorScheme.onSurface,
accent = MaterialTheme.colorScheme.primary,
),
illustrationSize = 180.dp,
title = "You're offline",
subtitle = "We'll sync everything as soon as you're back online.",
titleStyle = MaterialTheme.typography.titleMedium,
subtitleStyle = MaterialTheme.typography.bodyMedium,
)Every knob — color, size, copy, typography — is an ordinary parameter with a
sensible default. Omit title/subtitle entirely (pass null) for an
icon-only illustration. SketchyColors is the same type for both catalogs, so
one palette restyles your entire onboarding flow and empty-state set at once.
Icons are a simpler model than illustrations and empty states — monochrome by
design, one tint rather than a full palette, and static rather than
looping, since they're meant to sit inline in real UI (a button, a list row,
a nav bar) rather than as decoration:
import com.sketchy.library.icons.Icon
import com.sketchy.library.icons.SketchyIcon
SketchyIcon(
icon = Icon.Heart,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurface,
)strokeWidth is a design-space number on the same 24-unit grid the icon
itself is drawn on — it scales along with the icon if you display it larger
than the default 24dp. A one-shot fade/pop still plays on first composition
by default; pass animate = false to skip it and render at full opacity
immediately.
Every icon renders in five families off the same hand-drawn geometry — pick whichever fits the weight of the UI around it:
import com.sketchy.library.icons.IconStyle
SketchyIcon(icon = Icon.Bell, style = IconStyle.Default) // a hollow line at normal weight
SketchyIcon(icon = Icon.Bell, style = IconStyle.Outlined) // the same line, drawn thinner
SketchyIcon(icon = Icon.Bell, style = IconStyle.Sharp) // square corners, mitred joins
SketchyIcon(icon = Icon.Bell, style = IconStyle.Filled) // a solid silhouette
SketchyIcon(icon = Icon.Bell, style = IconStyle.TwoTone) // Filled at low opacity, Default traced on topA handful of situations — a network error, an empty result set, "all caught up" — are drawn in more than one visual family, so you pick whichever fits your app instead of being stuck with one look:
NoInternet, EmptyCart, AllDone,
…). The default, general-purpose style, covering four categories:
Connectivity & Errors, Content & Search, Saved & Commerce, and Everyday &
Productivity.Signboard* — road signage as the scene's setting: signals, warning
and stop signs, cones, boards.Network* — network hardware only: routers, servers, cables — no
abstract icon glyphs.LinedMan* — a single minimal outline figure. The body never changes;
only the eyes and mouth do, to carry the mood.Every prefixed family names its entries {Family}{Situation}, so the family
is right there in the enum: LinedManLoading, NetworkNoInternet,
SignboardNetworkError. Where a situation is covered by more than one
family, here's the cross-reference:
| Situation | Original catalog | Signboards | Network | Lined Man |
|---|---|---|---|---|
| Page not found (404) | PageNotFound |
SignboardPageNotFound |
NetworkPageNotFound |
— |
| Network / connection error |
NoInternet · ServerError
|
SignboardNetworkError |
NetworkNoInternet · NetworkBadGateway · NetworkServerError · NetworkUnsecureWifi
|
LinedManNoConnection · LinedManSomethingWrong
|
| No data / no results |
NoResults · NoData
|
SignboardNoData |
NetworkNoData · NetworkNoResults
|
LinedManNoResults |
| Access denied | — |
SignboardUnauthorized · SignboardForbidden
|
— | LinedManAccessDenied |
| Under maintenance / coming soon | UnderMaintenance |
SignboardMaintenance · SignboardComingSoon
|
— | — |
| No messages / comments |
NoMessages · NoComments
|
— |
NetworkNoMessages · NetworkNoComments
|
— |
| Empty inbox | EmptyInbox |
— | — | LinedManEmptyInbox |
| All caught up / all clear |
AllDone · NoNotifications
|
SignboardAllClear · SignboardNoWarnings
|
— |
LinedManAllDone · LinedManAllCaughtUp
|
Swapping styles is a one-line change — same parameters, same colors
override, same optional title/subtitle:
// The default style
SketchyEmptyState(state = EmptyState.NoInternet)
// The same situation, drawn as network hardware instead
SketchyEmptyState(state = EmptyState.NetworkNoInternet)
// ...or as the minimal Lined Man figure
SketchyEmptyState(state = EmptyState.LinedManNoConnection)Every other entry (EmptyCart, LinedManWelcome, NetworkNoList, …) is
currently exclusive to its family — a good spot to contribute a matching
scene in a different style.
Every entry below draws both ways — colourless by default, painted with
colorful = true.
Sketch |
Scene |
|---|---|
Sketch.WarmWelcome |
A Warm Welcome |
Sketch.MorningCoffee |
A Slow Morning Coffee |
Sketch.HomeWorkspace |
Your Workspace at Home |
Sketch.GroceryRun |
The Weekly Grocery Run |
Sketch.ReadingNook |
A Quiet Reading Corner |
Sketch.RainyWindow |
Rainy Day Indoors |
| Category | Illustrations |
|---|---|
| Weather | A Rainy Day |
| Plants and Things | A Desert in Bloom |
| Signboards | Road Work Ahead · Every Path Leads Somewhere |
| Productivity | Know What Matters Most · Make Time For What Matters · Get More Done Together · Let Automation Handle It · Reflect, Then Reset · Plan Every Task · Find Your Focus · Never Miss a Meeting · Capture Every Thought · Build Better Habits |
| Finance | Track Every Expense · Watch Your Savings Grow |
| Fitness | Train Anywhere, Anytime · See Your Progress |
| Food Delivery | Order Your Favorites · Fast, Fresh Delivery |
| Travel | Plan Your Perfect Trip · Explore The World |
| Music | Your Soundtrack, Anywhere · Discover New Sounds |
The same cloud mascot every time — only its face and the sky around it (a peeking sun, a crescent moon, rain, lightning, a rainbow) change:
| Category | Empty states |
|---|---|
| Weather | No Connection · Something Went Wrong · All Caught Up · All Quiet · No Results Found · Just a Moment · Nothing Scheduled Tonight · Welcome! · Your Inbox is Empty · Under Maintenance |
The same twin-bell alarm clock every time — only its face, hands and the small prop beside it change:
| Category | Empty states |
|---|---|
| Clock and Time | All Quiet · No Alarms Set · Nothing Scheduled · No Reminders Yet · Session Expired · Time's Up · All Caught Up · Coming Soon |
A small garden world built only from potted plants and garden props — terracotta pots, watering cans, plant markers:
| Category | Empty states |
|---|---|
| Plants and Things | No Data Yet · No Favorites Yet · No Results Found · Nothing To Do · All Quiet · All Caught Up · No Messages Yet · Your Cart is Empty · Let's Get Growing · Under Maintenance |
A minimal single-outline standing figure — the same body every time, only the eyes and mouth change to carry the mood:
| Category | Empty states |
|---|---|
| Lined Man | All Caught Up · Something Went Wrong · Loading · No Connection · Access Denied · All Done · No Results Found · Your Inbox is Empty · Welcome! · Just a Moment · Taking a Break |
Road signage as the scene's setting — signals, warning/stop signs, cones, boards:
| Category | Empty states |
|---|---|
| Signboards | Page Not Found · Network Error · No Data Available · Sign In Required · Access Denied · Under Maintenance · All Clear · No Warnings · Coming Soon · Nothing Posted Yet · End of the Road |
Network hardware only — routers, servers, cables, no abstract icons:
| Category | Empty states |
|---|---|
| Network | No Internet Connection · Page Not Found · Bad Gateway · Something Went Wrong · Unsecured Connection · No Data Available · Nothing Connected · No Messages Yet · No Comments Yet · No Results Found |
And the original catalog:
| Category | Empty states |
|---|---|
| Connectivity & Errors | No Internet · Server Error · Sync Failed · Under Maintenance · Location Not Found |
| Content & Search | No Results · No Data · No Comments · No Messages · Page Not Found (404) |
| Saved & Commerce | Empty Cart · Empty Wishlist · No Favorites · No Bookmarks · No Downloads |
| Everyday & Productivity | Empty Inbox · No Notifications · Empty Calendar · No Photos · All Done |
Monochrome, hand-drawn glyphs on the standard 24×24 icon grid — pass any IconStyle to
SketchyIcon to switch families:
| Category | Icons |
|---|---|
| General | Home · Search · Heart · Star · Bell · Settings · User · Mail · Calendar · Clock · Cart · Edit · Trash · Camera · Image · Folder · Download · Upload · Share · Lock |
Sketchy/
├── library/ # The published artifact — pure Compose, no app dependencies
│ └── src/commonMain/kotlin/com/sketchy/library/
│ ├── SketchyColors.kt # Shared reskinnable palette (both catalogs)
│ ├── SketchyStyle.kt # Outlined vs. painted — how one scene renders both ways
│ ├── illustrations/
│ │ ├── SketchyIllustrations.kt # Sketch enum, SketchyIllustration composable
│ │ ├── Featured*.kt # …the elaborate full scenes, one file each
│ │ └── Onboarding*.kt # …grouped by category, 2-5 scenes each
│ ├── emptystates/
│ │ ├── EmptyState.kt # EmptyState enum, SketchyEmptyState composable
│ │ └── EmptyStates*.kt # …grouped by category
│ ├── icons/
│ │ ├── Icons.kt # Icon enum, IconStyle, SketchyIcon composable
│ │ ├── IconPainting.kt # Shared stroke/fill/hole rendering per IconStyle
│ │ └── Icons*.kt # …grouped by category
│ └── utils/
│ ├── Extensions.kt # DrawScope drawing extensions (stroke, sketchLine, …)
│ ├── Painting.kt # Fills, shading, brushes, limbs — the painted half
│ └── Utils.kt # Ink/accent color constants, wave(), DesignSize
├── composeApp/ # Demo UI — searchable, categorized gallery of everything above,
│ │ # shared across all four platforms
│ ├── src/commonMain/kotlin/ # The catalog screens
│ ├── src/androidMain/kotlin/ # Clipboard + Material You actuals
│ ├── src/jvmMain/kotlin/ # Desktop entry point (main.kt)
│ ├── src/iosMain/kotlin/ # iOS entry point (MainViewController.kt)
│ └── src/wasmJsMain/kotlin/ # Web entry point (main.kt)
├── androidApp/ # Thin Android shell — Activity, manifest, launcher resources.
│ # Separate because AGP 9 won't apply com.android.application
│ # alongside the Kotlin Multiplatform plugin.
└── iosApp/ # Xcode project hosting the shared Compose UI
New illustrations are very welcome. A new scene is just an internal
DrawScope function plus one enum entry — no boilerplate beyond that:
Sketch
or EmptyState enum with its display copy and category.internal fun DrawScope.drawYourScene(t: Float, colors: SketchyStyle)
in the matching illustrations/Onboarding*.kt or emptystates/EmptyStates*.kt
file, reusing the shared extensions from utils/Extensions.kt (stroke,
sketchLine, sketchCircle, twinkle, wave, groundHint/groundLine)
for a consistent hand-drawn look — and always paint from colors.*
(never the raw Ink/Accent constants in utils/Utils.kt) so the scene
stays themeable.when dispatcher and open a PR.Please keep new scenes framework-agnostic (no Material/Material3 imports
inside :library) so the empty-state API stays usable in any design system.
Sketchy is available under the MIT License.
❤️ Created with love by Muhammad Ali
"Empty screens deserve better than a spinner."
Hand-drawn, animated illustrations & empty states for Compose Multiplatform.
Sketchy is a Kotlin Multiplatform library of sketch-style, line-art artwork —
the kind of warm, human illustration you'd want for onboarding flows and empty
states — drawn entirely on Canvas, animated out of the box, and fully
themeable to match your app. It runs on Android, iOS, desktop and the web from
one commonMain source set.
| Target | Status |
|---|---|
| Android | minSdk 26 |
| iOS |
iosArm64, iosSimulatorArm64
|
| Desktop (JVM) | Windows, macOS, Linux |
| Web |
wasmJs (browser) |
iosX64 (the Intel simulator) is not supported — Compose Multiplatform no
longer publishes artifacts for it.
https://github.com/user-attachments/assets/2db98caf-7201-4e06-97de-919f964268de
Most illustration libraries ship static SVGs or Lottie files — dependencies outside Compose's own rendering pipeline, hard to restyle, and rarely animated in a way that feels alive. Sketchy takes a different approach:
DrawScope, so it's crisp at any size and ships as pure Kotlin
— no drawables, no Lottie JSON, no binary bloat.animate = false when you want
a static frame instead.colorful = true and the same
scene is painted in full: shaded surfaces, contact shadows, highlights. No
second set of artwork, and never a background of its own either way.SketchyColors, Dp, String, TextStyle) — no XML
theming, no design-system lock-in. It doesn't even depend on Material.| Catalog | Count | Entry point |
|---|---|---|
| Featured illustrations | 6 elaborate full scenes | SketchyIllustration(modifier, sketch, animate, colorful, colors) |
| Onboarding illustrations | 24, across 9 categories | SketchyIllustration(modifier, sketch, animate, colorful, colors) |
| Empty states | 80, across 10 categories | SketchyEmptyState(state, modifier, animate, colorful, colors, illustrationSize, title, subtitle, titleStyle, subtitleStyle, spacing) |
| Icons | 20, across 1 category | SketchyIcon(icon, modifier, style, tint, animate) |
Every one of the 110 illustrations and empty states draws two ways from the same code — a
colourless hand-drawn outline by default, or fully painted with colorful = true. Icons are a
different, simpler model: monochrome by design (a single tint rather than the full palette), each
one rendering in five families off the same hand-drawn geometry — Default, Outlined, Sharp,
Filled and TwoTone.
A demo ships alongside the library as a live, searchable catalog of every
illustration and empty state — the fastest way to browse what's available and
copy the exact usage snippet for whatever you pick. Its UI lives in
:composeApp and is shared across all four platforms:
./gradlew :composeApp:run # desktop
./gradlew :androidApp:installDebug # Android
./gradlew :composeApp:wasmJsBrowserDevelopmentRun # web
open iosApp/iosApp.xcodeproj # iOS (run from Xcode)Sketchy never ships two sets of artwork. Each scene is written once, and the
colorful flag decides how the palette hands its colours back to the drawing
code:
colorful = false (default)
|
colorful = true |
|
|---|---|---|
| Look | Pure line-art — ink outlines, nothing filled in | Gradient-shaded surfaces, contact shadows, highlights, under the same outlines |
| Palette slots used |
ink, inkSoft, inkFaint + the four accents |
All of the above plus the light and material slots |
| Colour | Only small accent marks — a sparkle, a glint, one highlighted bar | Full colour throughout |
| Background | Transparent | Transparent |
| Best for | Minimal / monochrome UIs, dark mode, matching a single brand ink | Onboarding heroes, marketing screens, playful apps |
Neither mode ever paints a background of its own, so a scene drops onto whatever surface your screen already has.
How it works: when a scene is outlined, every surface, light and material slot
of the palette reads back as Color.Transparent, so the fill simply isn't
painted and what survives is the ink outline of the same shape. That's why
colorful is one boolean and not a second catalog.
Sketchy is live on Maven Central as io.github.ali0092:sketchy — the version
badge above always reflects the latest published release.
A multiplatform consumer adds it to commonMain — mavenCentral() is already
declared by default in a Kotlin Multiplatform project's repositories, no extra
repository setup required:
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.ali0092:sketchy:1.0.9")
}
}
}An Android-only consumer adds it the usual way:
// app/build.gradle.kts
dependencies {
implementation("io.github.ali0092:sketchy:1.0.9")
}Sketchy is also mirrored on JitPack, kept at the same version and coordinate as Maven Central, as a fallback in case Maven Central isn't reachable from your network:
dependencies {
implementation("io.github.ali0092:sketchy:1.0.9")
}with the JitPack repository declared in settings.gradle.kts:
dependencyResolutionManagement {
repositories {
mavenCentral()
maven("https://jitpack.io")
}
}import com.sketchy.library.illustrations.Sketch
import com.sketchy.library.illustrations.SketchyIllustration
SketchyIllustration(
sketch = Sketch.PlanTasks,
modifier = Modifier.size(280.dp)
)Every Sketch scales to whatever size you give it and animates on a loop by
default. Pass animate = false to freeze it on its resting frame — handy for
lower-power devices or a still hero image.
// Colourless — the default. Ink outlines, a few accent marks, nothing filled in.
SketchyIllustration(
sketch = Sketch.MorningCoffee,
modifier = Modifier.size(280.dp),
)
// Colourful — the same scene, painted: shaded surfaces, shadows, highlights.
SketchyIllustration(
sketch = Sketch.MorningCoffee,
modifier = Modifier.size(280.dp),
colorful = true,
)colorful works on every illustration and every empty state, and it's an
ordinary parameter — drive it from a setting, from isSystemInDarkTheme(), or
from a single app-wide constant:
SketchyEmptyState(
state = EmptyState.EmptyCart,
colorful = !isSystemInDarkTheme(),
)The six Featured scenes are the elaborate ones — a whole scene rather than a single motif, one source file each, built on the shading primitives the painted mode uses. They're the ones worth giving a full-bleed hero slot:
SketchyIllustration(
sketch = Sketch.ReadingNook, // or WarmWelcome, MorningCoffee, HomeWorkspace, GroceryRun, RainyWindow
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
colorful = true,
)They take exactly the same parameters as every other Sketch — animate,
colorful and colors all behave identically — and they render colourless
just as happily if that's your look. Because they carry the most detail, they
also gain the most from colorful = true, so they're the best place to start
if you're deciding between the two modes.
colors is a plain SketchyColors parameter — override any subset of it and
every stroke, fill, and sparkle in the scene repaints to match, no XML themes
or design-system lock-in required.
A colourless scene only ever draws from the first block of the palette — ink,
inkSoft, inkFaint and the four accents (accent, accentGreen,
accentBlue, accentRed). The light and material slots below them are what
the colourful version fills with, so reskinning a colourless app means changing
the ink and the accents and nothing else:
import com.sketchy.library.SketchyColors
import com.sketchy.library.illustrations.Sketch
import com.sketchy.library.illustrations.SketchyIllustration
SketchyIllustration(
sketch = Sketch.BuildBetterHabits,
modifier = Modifier.size(280.dp),
colors = SketchyColors(
ink = MaterialTheme.colorScheme.onSurface,
accent = MaterialTheme.colorScheme.primary,
accentBlue = MaterialTheme.colorScheme.secondary,
)
)If you use colorful = true, the rest of the palette is worth a look too —
it's grouped so you can retint a whole painted scene a few slots at a time:
| Block | Slots | What it paints |
|---|---|---|
| Ink |
ink, inkSoft, inkFaint
|
Every outline, in both modes |
| Accents |
accent, accentGreen, accentBlue, accentRed
|
Small coloured marks, in both modes |
| Light & atmosphere |
paper, sun, sunDeep, glow, sky, skyDeep, shade, shadeSoft
|
Key light, bloom, shadows — colourful only |
| Materials |
wood, woodDark, leaf, leafDark, terracotta, clay, fabric, fabricDark, metal, metalDark, skin, skinDark, hair, coffee
|
The surfaces themselves — colourful only |
Overriding a material slot while a scene is colourless is harmless — it's simply never read.
import com.sketchy.library.SketchyColors
import com.sketchy.library.emptystates.EmptyState
import com.sketchy.library.emptystates.SketchyEmptyState
SketchyEmptyState(
state = EmptyState.NoInternet,
colors = SketchyColors(
ink = MaterialTheme.colorScheme.onSurface,
accent = MaterialTheme.colorScheme.primary,
),
illustrationSize = 180.dp,
title = "You're offline",
subtitle = "We'll sync everything as soon as you're back online.",
titleStyle = MaterialTheme.typography.titleMedium,
subtitleStyle = MaterialTheme.typography.bodyMedium,
)Every knob — color, size, copy, typography — is an ordinary parameter with a
sensible default. Omit title/subtitle entirely (pass null) for an
icon-only illustration. SketchyColors is the same type for both catalogs, so
one palette restyles your entire onboarding flow and empty-state set at once.
Icons are a simpler model than illustrations and empty states — monochrome by
design, one tint rather than a full palette, and static rather than
looping, since they're meant to sit inline in real UI (a button, a list row,
a nav bar) rather than as decoration:
import com.sketchy.library.icons.Icon
import com.sketchy.library.icons.SketchyIcon
SketchyIcon(
icon = Icon.Heart,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurface,
)strokeWidth is a design-space number on the same 24-unit grid the icon
itself is drawn on — it scales along with the icon if you display it larger
than the default 24dp. A one-shot fade/pop still plays on first composition
by default; pass animate = false to skip it and render at full opacity
immediately.
Every icon renders in five families off the same hand-drawn geometry — pick whichever fits the weight of the UI around it:
import com.sketchy.library.icons.IconStyle
SketchyIcon(icon = Icon.Bell, style = IconStyle.Default) // a hollow line at normal weight
SketchyIcon(icon = Icon.Bell, style = IconStyle.Outlined) // the same line, drawn thinner
SketchyIcon(icon = Icon.Bell, style = IconStyle.Sharp) // square corners, mitred joins
SketchyIcon(icon = Icon.Bell, style = IconStyle.Filled) // a solid silhouette
SketchyIcon(icon = Icon.Bell, style = IconStyle.TwoTone) // Filled at low opacity, Default traced on topA handful of situations — a network error, an empty result set, "all caught up" — are drawn in more than one visual family, so you pick whichever fits your app instead of being stuck with one look:
NoInternet, EmptyCart, AllDone,
…). The default, general-purpose style, covering four categories:
Connectivity & Errors, Content & Search, Saved & Commerce, and Everyday &
Productivity.Signboard* — road signage as the scene's setting: signals, warning
and stop signs, cones, boards.Network* — network hardware only: routers, servers, cables — no
abstract icon glyphs.LinedMan* — a single minimal outline figure. The body never changes;
only the eyes and mouth do, to carry the mood.Every prefixed family names its entries {Family}{Situation}, so the family
is right there in the enum: LinedManLoading, NetworkNoInternet,
SignboardNetworkError. Where a situation is covered by more than one
family, here's the cross-reference:
| Situation | Original catalog | Signboards | Network | Lined Man |
|---|---|---|---|---|
| Page not found (404) | PageNotFound |
SignboardPageNotFound |
NetworkPageNotFound |
— |
| Network / connection error |
NoInternet · ServerError
|
SignboardNetworkError |
NetworkNoInternet · NetworkBadGateway · NetworkServerError · NetworkUnsecureWifi
|
LinedManNoConnection · LinedManSomethingWrong
|
| No data / no results |
NoResults · NoData
|
SignboardNoData |
NetworkNoData · NetworkNoResults
|
LinedManNoResults |
| Access denied | — |
SignboardUnauthorized · SignboardForbidden
|
— | LinedManAccessDenied |
| Under maintenance / coming soon | UnderMaintenance |
SignboardMaintenance · SignboardComingSoon
|
— | — |
| No messages / comments |
NoMessages · NoComments
|
— |
NetworkNoMessages · NetworkNoComments
|
— |
| Empty inbox | EmptyInbox |
— | — | LinedManEmptyInbox |
| All caught up / all clear |
AllDone · NoNotifications
|
SignboardAllClear · SignboardNoWarnings
|
— |
LinedManAllDone · LinedManAllCaughtUp
|
Swapping styles is a one-line change — same parameters, same colors
override, same optional title/subtitle:
// The default style
SketchyEmptyState(state = EmptyState.NoInternet)
// The same situation, drawn as network hardware instead
SketchyEmptyState(state = EmptyState.NetworkNoInternet)
// ...or as the minimal Lined Man figure
SketchyEmptyState(state = EmptyState.LinedManNoConnection)Every other entry (EmptyCart, LinedManWelcome, NetworkNoList, …) is
currently exclusive to its family — a good spot to contribute a matching
scene in a different style.
Every entry below draws both ways — colourless by default, painted with
colorful = true.
Sketch |
Scene |
|---|---|
Sketch.WarmWelcome |
A Warm Welcome |
Sketch.MorningCoffee |
A Slow Morning Coffee |
Sketch.HomeWorkspace |
Your Workspace at Home |
Sketch.GroceryRun |
The Weekly Grocery Run |
Sketch.ReadingNook |
A Quiet Reading Corner |
Sketch.RainyWindow |
Rainy Day Indoors |
| Category | Illustrations |
|---|---|
| Weather | A Rainy Day |
| Plants and Things | A Desert in Bloom |
| Signboards | Road Work Ahead · Every Path Leads Somewhere |
| Productivity | Know What Matters Most · Make Time For What Matters · Get More Done Together · Let Automation Handle It · Reflect, Then Reset · Plan Every Task · Find Your Focus · Never Miss a Meeting · Capture Every Thought · Build Better Habits |
| Finance | Track Every Expense · Watch Your Savings Grow |
| Fitness | Train Anywhere, Anytime · See Your Progress |
| Food Delivery | Order Your Favorites · Fast, Fresh Delivery |
| Travel | Plan Your Perfect Trip · Explore The World |
| Music | Your Soundtrack, Anywhere · Discover New Sounds |
The same cloud mascot every time — only its face and the sky around it (a peeking sun, a crescent moon, rain, lightning, a rainbow) change:
| Category | Empty states |
|---|---|
| Weather | No Connection · Something Went Wrong · All Caught Up · All Quiet · No Results Found · Just a Moment · Nothing Scheduled Tonight · Welcome! · Your Inbox is Empty · Under Maintenance |
The same twin-bell alarm clock every time — only its face, hands and the small prop beside it change:
| Category | Empty states |
|---|---|
| Clock and Time | All Quiet · No Alarms Set · Nothing Scheduled · No Reminders Yet · Session Expired · Time's Up · All Caught Up · Coming Soon |
A small garden world built only from potted plants and garden props — terracotta pots, watering cans, plant markers:
| Category | Empty states |
|---|---|
| Plants and Things | No Data Yet · No Favorites Yet · No Results Found · Nothing To Do · All Quiet · All Caught Up · No Messages Yet · Your Cart is Empty · Let's Get Growing · Under Maintenance |
A minimal single-outline standing figure — the same body every time, only the eyes and mouth change to carry the mood:
| Category | Empty states |
|---|---|
| Lined Man | All Caught Up · Something Went Wrong · Loading · No Connection · Access Denied · All Done · No Results Found · Your Inbox is Empty · Welcome! · Just a Moment · Taking a Break |
Road signage as the scene's setting — signals, warning/stop signs, cones, boards:
| Category | Empty states |
|---|---|
| Signboards | Page Not Found · Network Error · No Data Available · Sign In Required · Access Denied · Under Maintenance · All Clear · No Warnings · Coming Soon · Nothing Posted Yet · End of the Road |
Network hardware only — routers, servers, cables, no abstract icons:
| Category | Empty states |
|---|---|
| Network | No Internet Connection · Page Not Found · Bad Gateway · Something Went Wrong · Unsecured Connection · No Data Available · Nothing Connected · No Messages Yet · No Comments Yet · No Results Found |
And the original catalog:
| Category | Empty states |
|---|---|
| Connectivity & Errors | No Internet · Server Error · Sync Failed · Under Maintenance · Location Not Found |
| Content & Search | No Results · No Data · No Comments · No Messages · Page Not Found (404) |
| Saved & Commerce | Empty Cart · Empty Wishlist · No Favorites · No Bookmarks · No Downloads |
| Everyday & Productivity | Empty Inbox · No Notifications · Empty Calendar · No Photos · All Done |
Monochrome, hand-drawn glyphs on the standard 24×24 icon grid — pass any IconStyle to
SketchyIcon to switch families:
| Category | Icons |
|---|---|
| General | Home · Search · Heart · Star · Bell · Settings · User · Mail · Calendar · Clock · Cart · Edit · Trash · Camera · Image · Folder · Download · Upload · Share · Lock |
Sketchy/
├── library/ # The published artifact — pure Compose, no app dependencies
│ └── src/commonMain/kotlin/com/sketchy/library/
│ ├── SketchyColors.kt # Shared reskinnable palette (both catalogs)
│ ├── SketchyStyle.kt # Outlined vs. painted — how one scene renders both ways
│ ├── illustrations/
│ │ ├── SketchyIllustrations.kt # Sketch enum, SketchyIllustration composable
│ │ ├── Featured*.kt # …the elaborate full scenes, one file each
│ │ └── Onboarding*.kt # …grouped by category, 2-5 scenes each
│ ├── emptystates/
│ │ ├── EmptyState.kt # EmptyState enum, SketchyEmptyState composable
│ │ └── EmptyStates*.kt # …grouped by category
│ ├── icons/
│ │ ├── Icons.kt # Icon enum, IconStyle, SketchyIcon composable
│ │ ├── IconPainting.kt # Shared stroke/fill/hole rendering per IconStyle
│ │ └── Icons*.kt # …grouped by category
│ └── utils/
│ ├── Extensions.kt # DrawScope drawing extensions (stroke, sketchLine, …)
│ ├── Painting.kt # Fills, shading, brushes, limbs — the painted half
│ └── Utils.kt # Ink/accent color constants, wave(), DesignSize
├── composeApp/ # Demo UI — searchable, categorized gallery of everything above,
│ │ # shared across all four platforms
│ ├── src/commonMain/kotlin/ # The catalog screens
│ ├── src/androidMain/kotlin/ # Clipboard + Material You actuals
│ ├── src/jvmMain/kotlin/ # Desktop entry point (main.kt)
│ ├── src/iosMain/kotlin/ # iOS entry point (MainViewController.kt)
│ └── src/wasmJsMain/kotlin/ # Web entry point (main.kt)
├── androidApp/ # Thin Android shell — Activity, manifest, launcher resources.
│ # Separate because AGP 9 won't apply com.android.application
│ # alongside the Kotlin Multiplatform plugin.
└── iosApp/ # Xcode project hosting the shared Compose UI
New illustrations are very welcome. A new scene is just an internal
DrawScope function plus one enum entry — no boilerplate beyond that:
Sketch
or EmptyState enum with its display copy and category.internal fun DrawScope.drawYourScene(t: Float, colors: SketchyStyle)
in the matching illustrations/Onboarding*.kt or emptystates/EmptyStates*.kt
file, reusing the shared extensions from utils/Extensions.kt (stroke,
sketchLine, sketchCircle, twinkle, wave, groundHint/groundLine)
for a consistent hand-drawn look — and always paint from colors.*
(never the raw Ink/Accent constants in utils/Utils.kt) so the scene
stays themeable.when dispatcher and open a PR.Please keep new scenes framework-agnostic (no Material/Material3 imports
inside :library) so the empty-state API stays usable in any design system.
Sketchy is available under the MIT License.
❤️ Created with love by Muhammad Ali
"Empty screens deserve better than a spinner."