modal
1.0.2indexedFlexible, customizable modal component offering animated transitions, background blur/tint/scale, stacking multiple modals, gesture-driven dismissal, flexible positioning and visibility-ratio state for fine-grained control.
Flexible, customizable modal component offering animated transitions, background blur/tint/scale, stacking multiple modals, gesture-driven dismissal, flexible positioning and visibility-ratio state for fine-grained control.
A flexible and customizable modal component library for Compose Multiplatform with support for Android, iOS, Desktop, Web (JS), and WebAssembly.
| Platform | Status |
|---|---|
| Android | ✅ Supported |
| iOS | ✅ Supported (arm64, x64, simulatorArm64) |
| Desktop | ✅ Supported (JVM) |
| Web (JS) | ✅ Supported |
| WebAssembly | ✅ Supported |

Add the dependency to your build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.stetsiuk:compose-modal:1.0.2")
}
}
}
@Composable
fun App() {
ProvideModalComponentHost {
val modalState = rememberModalComponentState()
val scope = rememberCoroutineScope()
// Your app content
Column {
Text("Main Content")
Button(onClick = { scope.launch { modalState.show() } }) {
Text("Show Modal")
}
}
// Modal component
ModalComponent(
state = modalState,
onDismissRequest = { scope.launch { modalState.hide() } }
) {
Text(, modifier = Modifier.padding(dp))
}
}
}
@Composable
fun BottomSheetExample() {
ProvideModalComponentHost {
val state = rememberModalComponentState(initialVisibilityRatio = 0f)
val scope = rememberCoroutineScope()
MyAppContent()
ModalComponent(
state = state,
color = Color.White,
shape = RoundedCornerShape(topStart = dp, topEnd = dp),
hostConfigs = ModalComponentHostConfig.default(),
onDismissRequest = { scope.launch { state.hide() } }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(dp)
) {
Text(, style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(dp))
Text()
}
}
}
}
// Slide up animation
Image(
modifier = Modifier
.fillMaxWidth()
.offset {
IntOffset(
x = 0,
y = (200.dp - (200.dp * state.visibilityRatio)).roundToPx()
)
}
.alpha(state.visibilityRatio),
painter = painterResource(Res.drawable.image),
contentDescription = null
)
Main composable for creating modals.
@Composable
fun ModalComponent(
state: ModalComponentState,
modifier: Modifier = Modifier,
color: Color = Color.Transparent,
shape: = RectangleShape,
contentColor: = contentColorFor(color),
shadowElevation: Dp = dp,
tonalElevation: Dp = dp,
border: BorderStroke? = ,
hostConfigs: ModalComponentHostConfig = ModalComponentHostConfig.default(),
dismissOnBackPress: = ,
dismissOnClickOutside: = ,
onDismissRequest: () -> ,
content: () ->
)
Parameters:
State management for modal visibility and animations.
Factory function:
@Composable
fun rememberModalComponentState(
initialVisibilityRatio: Float = 0f
): ModalComponentState
Configuration for background visual effects.
data class ModalComponentHostConfig(
val contentAlignment: Alignment, // Alignment of modal content
val backgroundBlur: Dp, // Blur amount for background
val backgroundTint: Color, // Tint color for background
val backgroundScaleRatio: Float // Scale ratio for background (>1f zooms in)
)
Default configuration:
ModalComponentHostConfig.default()
Root composable that provides modal hosting capabilities.
@Composable
fun ProvideModalComponentHost(
modifier: Modifier = Modifier,
state: ModalComponentHostState = rememberModalComponentHostState(),
content: @Composable () -> Unit
)
You can stack multiple modals, and each will apply its visual effects cumulatively:
val states = List(3) { rememberModalComponentState(false) }
ProvideModalComponentHost {
MyContent()
states.forEachIndexed { index, state ->
ModalComponent(
state = state,
onDismissRequest = { scope.launch { state.hide() } }
) {
Text("Modal $index")
}
}
}
Create a custom configuration for different modal styles:
// Center dialog with strong blur
val centerDialogConfig = ModalComponentHostConfig(
contentAlignment = Alignment.Center,
backgroundBlur = 20.dp,
backgroundTint = Color.Black.copy(0.4f),
backgroundScaleRatio = 1.02f
)
// Side sheet with minimal effects
val sideSheetConfig = ModalComponentHostConfig(
contentAlignment = Alignment.CenterEnd,
backgroundBlur = 8.dp,
backgroundTint = Color.Black.copy(0.05f),
backgroundScaleRatio = 1.0f
)
Control animation timing and curves:
// Fast animation
scope.launch {
state.show(animationSpec = tween(150, easing = FastOutSlowInEasing))
}
// Spring animation
scope.launch {
state.show(animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
))
}
Use visibilityRatio for gesture-driven interactions:
Slider(
value = state.visibilityRatio,
onValueChange = { scope.launch { state.snapTo(it) } }
)
Copyright 2024 Vasyl Stetsiuk
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance the License.
You may obtain a copy of the License at
http:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.
Contributions are welcome! Please feel free to submit a Pull Request.
Vasyl Stetsiuk
state: State object controlling modal visibility and animationsmodifier: Modifier for the modal surfacecolor: Background color of the modalshape: Shape of the modal (e.g., RoundedCornerShape)contentColor: Color of content inside the modalshadowElevation: Elevation for shadowtonalElevation: Elevation for tonal overlayborder: Optional border for the modalhostConfigs: Configuration for background effectsdismissOnBackPress: Whether to dismiss on back button pressdismissOnClickOutside: Whether to dismiss when clicking outsideonDismissRequest: Callback invoked when dismiss is requestedcontent: Content to display inside the modalclass ModalComponentState(
initialVisibilityRatio: Float = 0f
) {
val visibilityRatio: Float // Current visibility ratio (0f to 1f)
val isVisible: Boolean // Whether modal is visible
val indexInStack: Int? // Position in modal stack
suspend fun show(animationSpec: AnimationSpec<Float> = tween(300))
suspend fun hide(animationSpec: AnimationSpec<Float> = tween(300))
suspend fun snapTo(targetValue: Float)
}
Surfaced from shared tags and platforms — no rankings paid for.