
Lightweight, fully customizable toast notifications featuring queueing with bounded size, action button with locale-aware label, swipe-to-dismiss, progress bar, custom animations, and optional native system toasts.
A lightweight, fully customizable toast notification library for Compose Multiplatform (Android & iOS).
Scaffold integration via ToastHost
Scaffold
Toast, iOS UIAlertController)| Tool | Version |
|---|---|
| Kotlin | 2.3+ |
| Compose Multiplatform | 1.10+ |
| Android min SDK | 24 |
| iOS | arm64 + Simulator arm64 |
Step 1. Add the version and library entry to gradle/libs.versions.toml:
[versions]
toastCompose = "latest version"
[libraries]
toast-compose = { module = "io.github.joyner-perez:toastcompose", version.ref = "toastCompose" }Step 2. Add the dependency in your build.gradle.kts:
Compose Multiplatform project — composeApp/build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.toast.compose)
}
}
}Android-only project — module build.gradle.kts:
dependencies {
implementation(libs.toast.compose)
}Compose Multiplatform project — composeApp/build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.joyner-perez:toastcompose:0.0.1")
}
}
}Android-only project — module build.gradle.kts:
dependencies {
implementation("io.github.joyner-perez:toastcompose:0.0.1")
}Gradle's variant selection automatically picks the correct artifact (AAR for Android, klib for iOS).
// 1. Create the state — survives recomposition and configuration changes
val toastState = rememberToastState()
// 2. Show a toast from a button click or any event handler
toastState.show("Operation successful", ToastType.SUCCESS)Never call show() directly in the composable body — it runs on every recomposition.
Use LaunchedEffect so the toast fires only when the value actually changes:
// ❌ Runs on every recomposition while error is non-blank
if (uiState.error.isNotBlank()) {
toastState.show(uiState.error, ToastType.ERROR)
}
// ✅ Fires only when uiState.error changes
LaunchedEffect(uiState.error) {
if (uiState.error.isNotBlank()) {
toastState.show(
message = uiState.error,
type = ToastType.ERROR,
onDismiss = { viewModel.clearError() }
)
}
}Pass ToastHost to the snackbarHost slot of Scaffold. It handles positioning and insets automatically.
@Composable
fun MyScreen() {
val toastState = rememberToastState()
Scaffold(
snackbarHost = { ToastHost(toastState = toastState) }
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Button(onClick = {
toastState.show("Operation successful", ToastType.SUCCESS)
}) { Text("Show SUCCESS") }
Button(onClick = {
toastState.show("An error occurred", ToastType.ERROR)
}) { Text("Show ERROR") }
Button(onClick = {
toastState.show("Important information", ToastType.INFO)
}) { Text("Show INFO") }
Button(onClick = {
toastState.show("Attention required", ToastType.WARNING)
}) { Text("Show WARNING") }
}
}
}Place ToastCompose inside a Box and align it manually:
@Composable
fun MyScreen() {
val toastState = rememberToastState()
Box(modifier = Modifier.fillMaxSize()) {
// Your screen content
Button(
modifier = Modifier.align(Alignment.Center),
onClick = { toastState.show("File saved", ToastType.SUCCESS) }
) { Text("Save") }
// Toast overlay — pinned to the bottom center
ToastCompose(
toastState = toastState,
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(horizontal = 16.dp, vertical = 24.dp)
)
}
}Four built-in types, each with a default icon and background color:
| Type | Color | Icon |
|---|---|---|
ToastType.SUCCESS |
Green #2E7D32
|
✅ CheckCircle |
ToastType.ERROR |
Red #C62828
|
❌ Close |
ToastType.INFO |
Blue #1565C0
|
ℹ️ Info |
ToastType.WARNING |
Orange #E65100
|
toastState.show("Operation successful", ToastType.SUCCESS)
toastState.show("An error occurred", ToastType.ERROR)
toastState.show("Important information", ToastType.INFO)
toastState.show("Attention required", ToastType.WARNING)Every visual property can be overridden per toast call:
// Custom vector icon (Material Icons or your own ImageVector)
toastState.show(
message = "Custom icon, color and font",
icon = ToastIcon.Vector(Icons.Filled.Star),
backgroundColor = Color(0xFF6A1B9A),
textColor = Color(0xFF00695C),
fontFamily = FontFamily.Cursive
)// Custom drawable / painter icon
// Android-only project → use painterResource from androidx
val painter = painterResource(R.drawable.ic_my_icon)
// Compose Multiplatform → use painterResource from compose-resources
val painter = painterResource(Res.drawable.ic_my_icon)
toastState.show(
message = "Toast with drawable icon",
icon = ToastIcon.Resource(painter = painter),
backgroundColor = Color(0xFF00695C)
)toastState.show(
message = "Hello",
type = ToastType.INFO, // controls default icon + color
durationMillis = 3000L, // display time in ms (default 2500)
icon = ToastIcon.Vector(...), // or ToastIcon.Resource(painter)
backgroundColor = Color.Blue,
textColor = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
iconTint = Color.Yellow,
iconSize = 32.dp,
shape = RoundedCornerShape(8.dp),
actionLabel = "Undo", // optional action button label
onAction = { /* on action tap */ }
)Pass onAction to show a tappable action button on the trailing side. If actionLabel is blank, the locale-aware default "Undo" label is used (supports English, Spanish, French, Italian, Portuguese, German).
// Default label "Undo" (locale-aware)
toastState.show(
message = "Item deleted",
type = ToastType.ERROR,
onAction = { /* restore item */ }
)
// Custom label
toastState.show(
message = "File saved",
type = ToastType.SUCCESS,
actionLabel = "View",
onAction = { openFile() }
)If a toast is already visible when show() is called, the new toast is queued and shown automatically once the current one is dismissed. The queue is bounded — extra toasts beyond the limit are silently dropped.
// Default max queue size is 3
val toastState = rememberToastState()
// Custom max queue size
val toastState = rememberToastState(maxQueueSize = 5)
// Enqueue multiple toasts at once
toastState.show("First toast in queue", ToastType.SUCCESS)
toastState.show("Second toast in queue", ToastType.INFO)
toastState.show("Third toast in queue", ToastType.WARNING)Show a thin animated bar at the bottom of the toast that depletes over durationMillis. Enable it once on ToastHost or ToastCompose and it applies to every toast.
// With Scaffold
Scaffold(
snackbarHost = {
ToastHost(
toastState = toastState,
showProgressBar = true
)
}
) { ... }
// Without Scaffold
ToastCompose(
toastState = toastState,
showProgressBar = true,
modifier = Modifier.align(Alignment.BottomCenter)
)Swipe left or right on any toast to dismiss it immediately. This behavior is built in and requires no extra setup.
Override the default slide-up / slide-down animations via the enter and exit parameters:
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
ToastHost(
toastState = toastState,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
)// Horizontal slide-in from the right
ToastHost(
toastState = toastState,
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut()
)Use the operating system's own toast mechanism (Android Toast, iOS UIAlertController) when you don't need Compose UI:
@Composable
fun MyScreen() {
val nativeToast = rememberToastNative()
Button(onClick = {
nativeToast.show("Short native toast") // SHORT by default
nativeToast.show("Long native toast", ToastNativeDuration.LONG) // or LONG
}) { Text("Show native toast") }
}Clear the current toast and drain the entire queue — useful when navigating away from a screen:
// Dismiss only the current toast (queue plays on)
toastState.dismiss()
// Dismiss current toast AND clear the queue
toastState.dismissAll()@Composable
fun rememberToastState(maxQueueSize: Int = 3): ToastStatefun show(
message : String,
type : ToastType = ToastType.INFO,
durationMillis : Long = 2500L,
icon : ToastIcon = ToastIcon.Vector(type.icon),
backgroundColor: Color = type.backgroundColor,
textColor : Color = Color.White,
fontFamily : FontFamily = FontFamily.Default,
fontSize : TextUnit = TextUnit.Unspecified,
iconTint : Color = Color.White,
fontWeight : FontWeight = FontWeight.Medium,
shape : Shape = RoundedCornerShape(12.dp),
iconSize : Dp = 28.dp,
actionLabel : String = "",
onAction : (() -> Unit)? = null
)@Composable
fun ToastHost(
toastState : ToastState,
modifier : Modifier = Modifier,
showProgressBar: Boolean = false,
enter : EnterTransition = slideInVertically(...) + fadeIn(),
exit : ExitTransition = slideOutVertically(...) + fadeOut()
)@Composable
fun ToastCompose(
toastState : ToastState,
modifier : Modifier = Modifier,
showProgressBar: Boolean = false,
enter : EnterTransition = slideInVertically(...) + fadeIn(),
exit : ExitTransition = slideOutVertically(...) + fadeOut()
)sealed class ToastIcon {
data class Vector(val imageVector: ImageVector) : ToastIcon() // Material icon or custom
data class Resource(val painter: Painter) : ToastIcon() // Drawable / painter
}fun ToastNative.show(message: String) // SHORT duration
fun ToastNative.show(message: String, duration: ToastNativeDuration) // SHORT or LONGMIT License
Copyright (c) 2026 Joyner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
A lightweight, fully customizable toast notification library for Compose Multiplatform (Android & iOS).
Scaffold integration via ToastHost
Scaffold
Toast, iOS UIAlertController)| Tool | Version |
|---|---|
| Kotlin | 2.3+ |
| Compose Multiplatform | 1.10+ |
| Android min SDK | 24 |
| iOS | arm64 + Simulator arm64 |
Step 1. Add the version and library entry to gradle/libs.versions.toml:
[versions]
toastCompose = "latest version"
[libraries]
toast-compose = { module = "io.github.joyner-perez:toastcompose", version.ref = "toastCompose" }Step 2. Add the dependency in your build.gradle.kts:
Compose Multiplatform project — composeApp/build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.toast.compose)
}
}
}Android-only project — module build.gradle.kts:
dependencies {
implementation(libs.toast.compose)
}Compose Multiplatform project — composeApp/build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.joyner-perez:toastcompose:0.0.1")
}
}
}Android-only project — module build.gradle.kts:
dependencies {
implementation("io.github.joyner-perez:toastcompose:0.0.1")
}Gradle's variant selection automatically picks the correct artifact (AAR for Android, klib for iOS).
// 1. Create the state — survives recomposition and configuration changes
val toastState = rememberToastState()
// 2. Show a toast from a button click or any event handler
toastState.show("Operation successful", ToastType.SUCCESS)Never call show() directly in the composable body — it runs on every recomposition.
Use LaunchedEffect so the toast fires only when the value actually changes:
// ❌ Runs on every recomposition while error is non-blank
if (uiState.error.isNotBlank()) {
toastState.show(uiState.error, ToastType.ERROR)
}
// ✅ Fires only when uiState.error changes
LaunchedEffect(uiState.error) {
if (uiState.error.isNotBlank()) {
toastState.show(
message = uiState.error,
type = ToastType.ERROR,
onDismiss = { viewModel.clearError() }
)
}
}Pass ToastHost to the snackbarHost slot of Scaffold. It handles positioning and insets automatically.
@Composable
fun MyScreen() {
val toastState = rememberToastState()
Scaffold(
snackbarHost = { ToastHost(toastState = toastState) }
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Button(onClick = {
toastState.show("Operation successful", ToastType.SUCCESS)
}) { Text("Show SUCCESS") }
Button(onClick = {
toastState.show("An error occurred", ToastType.ERROR)
}) { Text("Show ERROR") }
Button(onClick = {
toastState.show("Important information", ToastType.INFO)
}) { Text("Show INFO") }
Button(onClick = {
toastState.show("Attention required", ToastType.WARNING)
}) { Text("Show WARNING") }
}
}
}Place ToastCompose inside a Box and align it manually:
@Composable
fun MyScreen() {
val toastState = rememberToastState()
Box(modifier = Modifier.fillMaxSize()) {
// Your screen content
Button(
modifier = Modifier.align(Alignment.Center),
onClick = { toastState.show("File saved", ToastType.SUCCESS) }
) { Text("Save") }
// Toast overlay — pinned to the bottom center
ToastCompose(
toastState = toastState,
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(horizontal = 16.dp, vertical = 24.dp)
)
}
}Four built-in types, each with a default icon and background color:
| Type | Color | Icon |
|---|---|---|
ToastType.SUCCESS |
Green #2E7D32
|
✅ CheckCircle |
ToastType.ERROR |
Red #C62828
|
❌ Close |
ToastType.INFO |
Blue #1565C0
|
ℹ️ Info |
ToastType.WARNING |
Orange #E65100
|
toastState.show("Operation successful", ToastType.SUCCESS)
toastState.show("An error occurred", ToastType.ERROR)
toastState.show("Important information", ToastType.INFO)
toastState.show("Attention required", ToastType.WARNING)Every visual property can be overridden per toast call:
// Custom vector icon (Material Icons or your own ImageVector)
toastState.show(
message = "Custom icon, color and font",
icon = ToastIcon.Vector(Icons.Filled.Star),
backgroundColor = Color(0xFF6A1B9A),
textColor = Color(0xFF00695C),
fontFamily = FontFamily.Cursive
)// Custom drawable / painter icon
// Android-only project → use painterResource from androidx
val painter = painterResource(R.drawable.ic_my_icon)
// Compose Multiplatform → use painterResource from compose-resources
val painter = painterResource(Res.drawable.ic_my_icon)
toastState.show(
message = "Toast with drawable icon",
icon = ToastIcon.Resource(painter = painter),
backgroundColor = Color(0xFF00695C)
)toastState.show(
message = "Hello",
type = ToastType.INFO, // controls default icon + color
durationMillis = 3000L, // display time in ms (default 2500)
icon = ToastIcon.Vector(...), // or ToastIcon.Resource(painter)
backgroundColor = Color.Blue,
textColor = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
iconTint = Color.Yellow,
iconSize = 32.dp,
shape = RoundedCornerShape(8.dp),
actionLabel = "Undo", // optional action button label
onAction = { /* on action tap */ }
)Pass onAction to show a tappable action button on the trailing side. If actionLabel is blank, the locale-aware default "Undo" label is used (supports English, Spanish, French, Italian, Portuguese, German).
// Default label "Undo" (locale-aware)
toastState.show(
message = "Item deleted",
type = ToastType.ERROR,
onAction = { /* restore item */ }
)
// Custom label
toastState.show(
message = "File saved",
type = ToastType.SUCCESS,
actionLabel = "View",
onAction = { openFile() }
)If a toast is already visible when show() is called, the new toast is queued and shown automatically once the current one is dismissed. The queue is bounded — extra toasts beyond the limit are silently dropped.
// Default max queue size is 3
val toastState = rememberToastState()
// Custom max queue size
val toastState = rememberToastState(maxQueueSize = 5)
// Enqueue multiple toasts at once
toastState.show("First toast in queue", ToastType.SUCCESS)
toastState.show("Second toast in queue", ToastType.INFO)
toastState.show("Third toast in queue", ToastType.WARNING)Show a thin animated bar at the bottom of the toast that depletes over durationMillis. Enable it once on ToastHost or ToastCompose and it applies to every toast.
// With Scaffold
Scaffold(
snackbarHost = {
ToastHost(
toastState = toastState,
showProgressBar = true
)
}
) { ... }
// Without Scaffold
ToastCompose(
toastState = toastState,
showProgressBar = true,
modifier = Modifier.align(Alignment.BottomCenter)
)Swipe left or right on any toast to dismiss it immediately. This behavior is built in and requires no extra setup.
Override the default slide-up / slide-down animations via the enter and exit parameters:
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
ToastHost(
toastState = toastState,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
)// Horizontal slide-in from the right
ToastHost(
toastState = toastState,
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut()
)Use the operating system's own toast mechanism (Android Toast, iOS UIAlertController) when you don't need Compose UI:
@Composable
fun MyScreen() {
val nativeToast = rememberToastNative()
Button(onClick = {
nativeToast.show("Short native toast") // SHORT by default
nativeToast.show("Long native toast", ToastNativeDuration.LONG) // or LONG
}) { Text("Show native toast") }
}Clear the current toast and drain the entire queue — useful when navigating away from a screen:
// Dismiss only the current toast (queue plays on)
toastState.dismiss()
// Dismiss current toast AND clear the queue
toastState.dismissAll()@Composable
fun rememberToastState(maxQueueSize: Int = 3): ToastStatefun show(
message : String,
type : ToastType = ToastType.INFO,
durationMillis : Long = 2500L,
icon : ToastIcon = ToastIcon.Vector(type.icon),
backgroundColor: Color = type.backgroundColor,
textColor : Color = Color.White,
fontFamily : FontFamily = FontFamily.Default,
fontSize : TextUnit = TextUnit.Unspecified,
iconTint : Color = Color.White,
fontWeight : FontWeight = FontWeight.Medium,
shape : Shape = RoundedCornerShape(12.dp),
iconSize : Dp = 28.dp,
actionLabel : String = "",
onAction : (() -> Unit)? = null
)@Composable
fun ToastHost(
toastState : ToastState,
modifier : Modifier = Modifier,
showProgressBar: Boolean = false,
enter : EnterTransition = slideInVertically(...) + fadeIn(),
exit : ExitTransition = slideOutVertically(...) + fadeOut()
)@Composable
fun ToastCompose(
toastState : ToastState,
modifier : Modifier = Modifier,
showProgressBar: Boolean = false,
enter : EnterTransition = slideInVertically(...) + fadeIn(),
exit : ExitTransition = slideOutVertically(...) + fadeOut()
)sealed class ToastIcon {
data class Vector(val imageVector: ImageVector) : ToastIcon() // Material icon or custom
data class Resource(val painter: Painter) : ToastIcon() // Drawable / painter
}fun ToastNative.show(message: String) // SHORT duration
fun ToastNative.show(message: String, duration: ToastNativeDuration) // SHORT or LONGMIT License
Copyright (c) 2026 Joyner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.