
Emoji picker UI component with category tabs, searchable grid (name/shortcode/emoticon matching), skin-tone selector, pluggable recent-store, Noto SVG fallback, 18-locale metadata and accessibility support.
A Compose Multiplatform emoji picker. Works on Android, iOS, JVM/Desktop, and wasmJs from a single codebase.
Research in April 2026 confirmed there is no Compose Multiplatform emoji picker. kosi-libs/Emoji.kt provides the data layer and renderers across all CMP targets but does not ship a picker UI. Every existing "compose emoji picker" on GitHub (androidx.emoji2.emojipicker, Abhimanyu14/compose-emoji-picker, vanniktech/Emoji, and several others) is Android-only.
This library is a thin UI layer on top of org.kodein.emoji:emoji-compose-m3, giving you:
SkinTone1Emoji and SkinTone2Emoji
RecentEmojiStore (in-memory default, bring your own persistence)// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
// picker/build.gradle.kts (commonMain)
dependencies {
implementation("me.digitalby:kmp-emoji-picker:0.1.0")
}Kotlin Multiplatform produces one artifact per target (kmp-emoji-picker-android, -jvm, -iosarm64, -iossimulatorarm64, -iosx64, -wasm-js) plus a metadata artifact. Gradle's KMP plugin resolves the right one for your target automatically, so you only need to declare the kmp-emoji-picker coordinate in commonMain.
Each release is also mirrored to GitHub Packages. Authentication is required for reads — generate a Personal Access Token with the read:packages scope and expose it as GITHUB_TOKEN (with GITHUB_ACTOR set to your GitHub username), or set gpr.user and gpr.key in ~/.gradle/gradle.properties.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
maven {
url = uri("https://maven.pkg.github.com/digitalby/kmp-emoji-picker")
credentials {
username = providers.gradleProperty("gpr.user").orNull
?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull
?: System.getenv("GITHUB_TOKEN")
}
}
}
}import me.digitalby.emojipicker.EmojiPicker
import me.digitalby.emojipicker.rememberEmojiPickerState
@Composable
fun MyScreen(onInsert: (String) -> Unit) {
val state = rememberEmojiPickerState()
EmojiPicker(
state = state,
onEmojiSelected = { emoji -> onInsert(emoji.details.string) },
)
}compileSdk = 35, minSdk = 24)iosX64, iosArm64, iosSimulatorArm64)Tab / Shift+Tab: move between search, category tabs, and the gridEnter / Space: insert the focused emojiAlt+Enter: open the skin-tone selector for the focused emoji (when supported)Escape: dismiss the skin-tone popupEvery emoji cell carries contentDescription and Role.Button, so TalkBack, VoiceOver, and desktop screen readers announce the emoji name and "button" affordance. Tone chips carry per-tone descriptions (e.g. "waving hand, medium dark skin tone").
Emoji with skin-tone variants show a small disclosure triangle in the bottom-right corner of the cell (matching iOS and Android system pickers), and their contentDescription is suffixed with "has skin tone variants" so screen readers announce the affordance.
Per-emoji names and search keywords ship in 18 locales (en, ru, uk, be, zh, zh-hant, es, fr, de, pt, ja, ko, it, ar, hi, tr, pl, nl), generated from CLDR annotations via the :picker-codegen module.
Category tab labels ("Smileys & Emotion", "People", "Animals & Nature", etc.) ship in the same 18 locales, sourced from Apple's EmojiFoundation.framework/Localizable.loctable via the public applelocalization.com mirror of macOS/iOS system frameworks. The one label without a direct Apple equivalent — Unicode's smileys_emotion group — is hand-authored.
The active locale is detected automatically via each platform's input-method / system locale API. Override via -Demojipicker.forceLocale=<tag> for deterministic tests.
The sample/composeApp module dogfoods the picker on every target.
# Desktop (JVM)
./gradlew :sample:composeApp:run
# Android (device or emulator attached)
./gradlew :sample:composeApp:installDebug
# Web (wasmJs) — serves at http://localhost:8080
./gradlew :sample:composeApp:wasmJsBrowserDevelopmentRun
# iOS — compiles the framework; integrate into your own SwiftUI host
./gradlew :sample:composeApp:linkDebugFrameworkIosSimulatorArm64For iOS, the KMP module exports MainViewController(). Wire it into SwiftUI:
import SwiftUI
import ComposeApp
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ vc: UIViewController, context: Context) {}
}MIT
A Compose Multiplatform emoji picker. Works on Android, iOS, JVM/Desktop, and wasmJs from a single codebase.
Research in April 2026 confirmed there is no Compose Multiplatform emoji picker. kosi-libs/Emoji.kt provides the data layer and renderers across all CMP targets but does not ship a picker UI. Every existing "compose emoji picker" on GitHub (androidx.emoji2.emojipicker, Abhimanyu14/compose-emoji-picker, vanniktech/Emoji, and several others) is Android-only.
This library is a thin UI layer on top of org.kodein.emoji:emoji-compose-m3, giving you:
SkinTone1Emoji and SkinTone2Emoji
RecentEmojiStore (in-memory default, bring your own persistence)// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
// picker/build.gradle.kts (commonMain)
dependencies {
implementation("me.digitalby:kmp-emoji-picker:0.1.0")
}Kotlin Multiplatform produces one artifact per target (kmp-emoji-picker-android, -jvm, -iosarm64, -iossimulatorarm64, -iosx64, -wasm-js) plus a metadata artifact. Gradle's KMP plugin resolves the right one for your target automatically, so you only need to declare the kmp-emoji-picker coordinate in commonMain.
Each release is also mirrored to GitHub Packages. Authentication is required for reads — generate a Personal Access Token with the read:packages scope and expose it as GITHUB_TOKEN (with GITHUB_ACTOR set to your GitHub username), or set gpr.user and gpr.key in ~/.gradle/gradle.properties.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
maven {
url = uri("https://maven.pkg.github.com/digitalby/kmp-emoji-picker")
credentials {
username = providers.gradleProperty("gpr.user").orNull
?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull
?: System.getenv("GITHUB_TOKEN")
}
}
}
}import me.digitalby.emojipicker.EmojiPicker
import me.digitalby.emojipicker.rememberEmojiPickerState
@Composable
fun MyScreen(onInsert: (String) -> Unit) {
val state = rememberEmojiPickerState()
EmojiPicker(
state = state,
onEmojiSelected = { emoji -> onInsert(emoji.details.string) },
)
}compileSdk = 35, minSdk = 24)iosX64, iosArm64, iosSimulatorArm64)Tab / Shift+Tab: move between search, category tabs, and the gridEnter / Space: insert the focused emojiAlt+Enter: open the skin-tone selector for the focused emoji (when supported)Escape: dismiss the skin-tone popupEvery emoji cell carries contentDescription and Role.Button, so TalkBack, VoiceOver, and desktop screen readers announce the emoji name and "button" affordance. Tone chips carry per-tone descriptions (e.g. "waving hand, medium dark skin tone").
Emoji with skin-tone variants show a small disclosure triangle in the bottom-right corner of the cell (matching iOS and Android system pickers), and their contentDescription is suffixed with "has skin tone variants" so screen readers announce the affordance.
Per-emoji names and search keywords ship in 18 locales (en, ru, uk, be, zh, zh-hant, es, fr, de, pt, ja, ko, it, ar, hi, tr, pl, nl), generated from CLDR annotations via the :picker-codegen module.
Category tab labels ("Smileys & Emotion", "People", "Animals & Nature", etc.) ship in the same 18 locales, sourced from Apple's EmojiFoundation.framework/Localizable.loctable via the public applelocalization.com mirror of macOS/iOS system frameworks. The one label without a direct Apple equivalent — Unicode's smileys_emotion group — is hand-authored.
The active locale is detected automatically via each platform's input-method / system locale API. Override via -Demojipicker.forceLocale=<tag> for deterministic tests.
The sample/composeApp module dogfoods the picker on every target.
# Desktop (JVM)
./gradlew :sample:composeApp:run
# Android (device or emulator attached)
./gradlew :sample:composeApp:installDebug
# Web (wasmJs) — serves at http://localhost:8080
./gradlew :sample:composeApp:wasmJsBrowserDevelopmentRun
# iOS — compiles the framework; integrate into your own SwiftUI host
./gradlew :sample:composeApp:linkDebugFrameworkIosSimulatorArm64For iOS, the KMP module exports MainViewController(). Wire it into SwiftUI:
import SwiftUI
import ComposeApp
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ vc: UIViewController, context: Context) {}
}MIT