Making Compose Multiplatform apps feel at home: removing ripple animation on iOS

When developing apps for multiple platforms, providing a native experience is key to making users feel at home. Android users expect certain behaviors that differ from what iOS users are used to, and vice versa. One of these subtle, but noticeable differences is the ripple animation on button clicks.

In Android, the ripple effect is a well-established part of Material Design, giving users visual feedback on touch. However, on iOS, this animation feels foreign, as it doesn’t align with Apple's Human Interface Guidelines. Luckily, we are developing our cross-platform apps using Compose Multiplatform, and you can customize the behavior of your app to adapt to these platform-specific nuances.

In this post, we’ll walk you through how to conditionally show the ripple animation on Android and hide it on iOS, keeping your app feeling native on both platforms.

Create the Indication

The Indication represents visual effects that occur when certain interactions happen. This is the ripple effect by default in Material Design (and by extension in Compose Multiplatform). We will define these PlatformClickIndication and PlatformRippleTheme variable to be able to override them in each platform later using Kotlins's expect/actual mechanism.

import androidx.compose.foundation.Indication
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.Composable

@get:Composable
expect val PlatformClickIndication: Indication

@get:Composable
expect val PlatformRippleTheme: RippleTheme

commonMain/extensions/Indication.kt

Implement the Indication for each platform

Now we can implement the Indication that feels "at home" for each OS. In Android, it's quite straightforward. For iOS, we pretty much remove the ripple effect from happening.

import androidx.compose.foundation.Indication
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable

actual val PlatformClickIndication: Indication
    @Composable
    get() = rememberRipple()
    
actual val PlatformRippleTheme: RippleTheme
    @Composable
    get() = LocalRippleTheme.current

androidMain/extensions/Indication.android.kt

import androidx.compose.foundation.Indication
import androidx.compose.foundation.IndicationInstance
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope

private object NoIndication : Indication {

    private object NoIndicationInstance : IndicationInstance {
        override fun ContentDrawScope.drawIndication() {
            drawContent()
        }
    }
    
    @Composable
    override fun rememberUpdatedInstance(
         interactionSource: InteractionSource): IndicationInstance {
        return NoIndicationInstance
    }
}

actual val PlatformClickIndication: Indication
    @Composable
    get() = NoIndication
    
actual val PlatformRippleTheme: RippleTheme
    @Composable
    get() = object: RippleTheme {
    
        @Composable
        override fun defaultColor(): Color = Color.Green
        
        @Composable
        override fun rippleAlpha(): RippleAlpha = 
            RippleAlpha(0f, 0f, 0f, 0f)
    }

iosMain/extensions/Indication.ios.kt

Set it in the app-wide theme

The final step is to set these indications to the entire app so as not having to define them manually each time. We can achieve this by using a CompositionLocalProvider in the MaterialTheme we are (most probably) using in the app as follows.

    MaterialTheme(
        colorScheme = colorscheme,
        typography = typography,
        content = {
            CompositionLocalProvider(
                LocalIndication provides PlatformClickIndication,
                LocalRippleTheme provides PlatformRippleTheme,
                content = content
            )
        }
    )

commonMain/Theme.kt

That's it. Now by default the ripple effect will only appear in the Android but not in the iOS build making each app to feel at home.

Happy coding!