
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("Modal Content", modifier = Modifier.padding(24.dp))
}
}
}@Composable
fun BottomSheetExample() {
ProvideModalComponentHost {
val state = rememberModalComponentState(initialVisibilityRatio = 0f)
val scope = rememberCoroutineScope()
// Your content here
MyAppContent()
ModalComponent(
state = state,
color = Color.White,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
hostConfigs = ModalComponentHostConfig.default(),
onDismissRequest = { scope.launch { state.hide() } }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Text("Bottom Sheet Title", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
Text("Bottom sheet content goes here")
}
}
}
}// 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: Shape = RectangleShape,
contentColor: Color = contentColorFor(color),
shadowElevation: Dp = 0.dp,
tonalElevation: Dp = 0.dp,
border: BorderStroke? = null,
hostConfigs: ModalComponentHostConfig = ModalComponentHostConfig.default(),
dismissOnBackPress: Boolean = true,
dismissOnClickOutside: Boolean = true,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit
)Parameters:
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 modalState management for modal visibility and animations.
class 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)
}Factory function:
@Composable
fun rememberModalComponentState(
initialVisibilityRatio: Float = 0f
): ModalComponentStateConfiguration 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 with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributions are welcome! Please feel free to submit a Pull Request.
Vasyl Stetsiuk
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("Modal Content", modifier = Modifier.padding(24.dp))
}
}
}@Composable
fun BottomSheetExample() {
ProvideModalComponentHost {
val state = rememberModalComponentState(initialVisibilityRatio = 0f)
val scope = rememberCoroutineScope()
// Your content here
MyAppContent()
ModalComponent(
state = state,
color = Color.White,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
hostConfigs = ModalComponentHostConfig.default(),
onDismissRequest = { scope.launch { state.hide() } }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Text("Bottom Sheet Title", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
Text("Bottom sheet content goes here")
}
}
}
}// 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: Shape = RectangleShape,
contentColor: Color = contentColorFor(color),
shadowElevation: Dp = 0.dp,
tonalElevation: Dp = 0.dp,
border: BorderStroke? = null,
hostConfigs: ModalComponentHostConfig = ModalComponentHostConfig.default(),
dismissOnBackPress: Boolean = true,
dismissOnClickOutside: Boolean = true,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit
)Parameters:
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 modalState management for modal visibility and animations.
class 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)
}Factory function:
@Composable
fun rememberModalComponentState(
initialVisibilityRatio: Float = 0f
): ModalComponentStateConfiguration 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 with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributions are welcome! Please feel free to submit a Pull Request.
Vasyl Stetsiuk