slide-to-unlock
➡️ Fully customizable slide to unlock UI component for Jetpack Compose and KMP.
## 📷 Previews
Slide to Unlock is maintained by RevenueCat. RevenueCat SDK for Android allows you to implement in-app subscriptions and a paywall system on top of Google Play Billing. Also, anyone can contribute to improving code, docs, or something following our Contributing Guideline.
Download
Version Catalog
If you're using Version Catalog, you can configure the dependency by adding it to your libs.versions.toml
file as follows:
[versions]
#...
slidetounlock = "1.0.1"
[libraries]
#...
compose-slidetounlock = { module = "com.revenuecat.purchases:slide-to-unlock", version.ref = "slidetounlock" }
Gradle
Add the dependency below to your module's build.gradle.kts
file:
dependencies {
implementation("com.revenuecat.purchases:slide-to-unlock:$version")
// if you're using Version Catalog
implementation(libs.compose.slidetounlock)
}
For Kotlin Multiplatform, add the dependency below to your module's build.gradle.kts
file:
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.compose.slidetounlock)
}
}
}
Usage
You can easily implement a slide-to-unlock feature using the SlideToUnlock
composable. It offers intuitive customization options for colors, text, shapes, and even the entire content of the thumb and hint, allowing you to create a wide variety of styles. If you want to just directly dive into the sample codes, check out the demo project.
Basic Usage
The SlideToUnlock
composable exposes relevant state parameters, allowing you to hoist the slide status and track when the slide action is completed via a callback.
isSlided
: ABoolean
state that controls the component's state. Whentrue
, the thumb moves to the end and becomes disabled.onSlideCompleted
: A lambda that is invoked when the user successfully slides the thumb to the end. You should typically use this to update yourisSlided
state.
var isSlided by remember { mutableStateOf(false) }
SlideToUnlock(
isSlided = isSlided,
modifier = Modifier.fillMaxWidth(),
onSlideCompleted = { isSlided = true },
)
Customizing Colors
You can customize all colors of the component by leveraging an instance of DefaultSlideToUnlockColors
, which contains some prebuilt behaviors for providing proper color sets depending on the states, and you can pass it to the colors
parameter. This allows you to style the track, hint text, thumb, and progress indicator to match your app's theme.
var isSlided by remember { mutableStateOf(false) }
SlideToUnlock(
isSlided = isSlided,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = DefaultSlideToUnlockColors(
startTrackColor = Color.DarkGray,
endTrackColor = Color(0xFF11D483), // A green color
thumbColor = Color.White,
slidedHintColor = Color.White,
),
onSlideCompleted = { isSlided = true },
)
You can also provide a Brush
for the track background for more advanced gradient effects.
val colorStops = arrayOf(
0.0f to Color.Black,
1f to Color(0xDC393636),
)
SlideToUnlock(
isSlided = isSlided,
// ...
colors = DefaultSlideToUnlockColors(
trackBrush = Brush.verticalGradient(colorStops = colorStops),
// ...
),
onSlideCompleted = { isSlided = true },
)
With DefaultSlideToUnlockColors
, you can customize the properties below:
- startTrackColor: The color of the track when the thumb is at the start.
- endTrackColor: The color of the track when the thumb is at the end.
- trackBrush: An optional brush for the track, which overrides color if non-null.
- startHintColor: The color of the hint text at the start of the swipe.
- endHintColor: The color of the hint text at the end of the swipe (usually transparent).
- slidedHintColor: The color of the hint text after the slide is completed.
- thumbColor: The solid background color of the thumb.
- thumbIconColor: The color of the icon displayed inside the thumb.
- thumbBrush: An optional brush for the thumb.
- progressColor: The color used for the circular progress indicator.
While DefaultSlideToUnlockColors
offers a convenient way to set static and interpolated colors, you can achieve complete, dynamic control over the component's appearance by creating your own class that implements the SlideToUnlockColors
interface.
For example, you can create your own color sets that awares Material theme color scheme:
@Stable
private class MaterialThemedSlideToUnlockColors : SlideToUnlockColors {
@Composable
override fun trackColor(slideFraction: Float): Color {
// Linearly interpolate from the theme's surface color to the primary color.
return lerp(
start = MaterialTheme.colorScheme.surfaceVariant,
stop = MaterialTheme.colorScheme.primary,
fraction = (slideFraction / 0.85f).coerceIn(0f, 1f)
)
}
@Composable
override fun trackBrush(slideFraction: Float): Brush? = null
@Composable
override fun hintColor(slideFraction: Float): Color {
// Fade out the text color from onSurfaceVariant to transparent.
val startColor = MaterialTheme.colorScheme.onSurfaceVariant
return lerp(
start = startColor,
stop = startColor.copy(alpha = 0f),
fraction = (slideFraction / 0.45f).coerceIn(0f, 1f)
)
}
@Composable
override fun slidedHintColor(): Color {
return MaterialTheme.colorScheme.onPrimary
}
@Composable
override fun thumbColor(): Color {
return MaterialTheme.colorScheme.onPrimary
}
@Composable
override fun thumbBrush(slideFraction: Float): Brush? = null
@Composable
override fun thumbIconColor(): Color {
return MaterialTheme.colorScheme.primary
}
@Composable
override fun progressColor(): Color {
return MaterialTheme.colorScheme.onPrimary
}
}
Customizing Hint Texts
The text displayed in the track can be customized by passing a HintTexts
object. This allows you to define different messages for the initial state and the final "slided" state.
var isSlided by remember { mutableStateOf(false) }
SlideToUnlock(
isSlided = isSlided,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
hintTexts = HintTexts(
defaultText = "Slide to subscribe",
slidedText = "Subscribing...",
),
onSlideCompleted = { isSlided = true },
)
Fully Customize thumb
and hint
Composables
For complete control over the appearance and behavior of the thumb and hint, you can provide your own composable lambdas to the thumb
and hint
parameters. These are slot APIs that allow you to replace the default implementations entirely.
Customizing the Thumb
The thumb
slot provides you with the isSlided
state, the current slideFraction
, the colors
object, and the thumbSize
. You can use these to create dynamic and interactive thumbs.
This example replaces the default arrow icon with a rotating app icon and shows a success checkmark when completed.
var isSlided by remember { mutableStateOf(false) }
var isCompleted by remember { mutableStateOf(false) }
LaunchedEffect(isSlided) {
if (isSlided) {
delay(1500) // Simulate in-app purchases or any tasks
isCompleted = true
}
}
SlideToUnlock(
isSlided = isSlided,
// ...
onSlideCompleted = { isSlided = true },
thumb = { slided, fraction, colors, size ->
Box(
modifier = Modifier
.size(size)
.background(color = colors.thumbColor(), shape = CircleShape),
) {
if (isCompleted) {
Icon(
imageVector = Icons.Default.Done,
contentDescription = "Completed",
tint = Color(0xFF11D483), // Green for success
)
} else if (slided) {
CircularProgressIndicator(
modifier = Modifier.padding(8.dp),
color = colors.progressColor(),
strokeWidth = 3.dp,
)
} else {
Icon(
modifier = Modifier
.align(Alignment.Center)
.size(30.dp)
.rotate(fraction * -360), // Rotate the icon as the user slides
imageVector = Icons.Default.Restore,
tint = colors.thumbIconColor(),
contentDescription = "Slide to restore",
)
}
}
},
// ...
)
Customizing the Hint
Similarly, the hint
composable slot gives you full control over the content displayed in the track. You can add animations, different text styles, or even other components like progress indicators.
This example replaces the default text with a Row
that includes a CircularProgressIndicator
when the slide is completed.
SlideToUnlock(
isSlided = isSlided,
// ...
onSlideCompleted = { isSlided = true },
hint = { slided, fraction, hintTexts, colors, paddings ->
AnimatedContent(
modifier = Modifier.align(Alignment.Center),
targetState = isSlided,
) { isSlidedTarget ->
if (isSlidedTarget) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White,
strokeWidth = 2.dp,
)
Text(
modifier = Modifier.padding(start = 8.dp),
text = hintTexts.slidedText,
color = colors.slidedHintColor(),
style = MaterialTheme.typography.titleMedium,
)
}
} else {
Text(
text = hintTexts.defaultText,
color = colors.hintColor(fraction),
style = MaterialTheme.typography.titleMedium,
)
}
}
},
// ...
)
Customizing the Gesture Behavior
You can customize the behavior and feel of the slide gesture using the orientation
and fractionalThreshold
parameters.
Changing the Slide Orientation
By default, the component operates horizontally. You can change this to a vertical slide by setting the orientation
parameter to SlideOrientation.Vertical
.
When using a vertical orientation, it's important to provide appropriate size constraints to the component's modifier, as it will orient itself along the y-axis.
Example: A Vertical Slider
var isSlided by remember { mutableStateOf(false) }
SlideToUnlock(
isSlided = isSlided,
onSlideCompleted = { isSlided = true },
orientation = SlideOrientation.Vertical,
modifier = Modifier
.fillMaxHeight()
.width(80.dp) // A fixed width is recommended for vertical sliders
.padding(vertical = 32.dp)
)
Adjusting the Slide Threshold
The fractionalThreshold
parameter controls how far the user must drag the thumb before the slide action is considered complete upon release. It is a Float
value between 0.0
and 1.0
, representing the percentage of the track that must be covered.
The default value is 0.85f
(85%), which requires a deliberate gesture. You can lower this value to make the slide easier to complete.
Example: An Easier-to-Complete Slider
This slider will "snap" to the end and trigger onSlideCompleted
if the user drags it just past the halfway point.
var isSlided by remember { mutableStateOf(false) }
SlideToUnlock(
isSlided = isSlided,
onSlideCompleted = { isSlided = true },
// The slide will complete if the user drags past the 50% mark.
fractionalThreshold = 0.5f,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
Tracking Slide Fraction Changes
If you need to react to the slide progress in real-time, you can use the onSlideFractionChanged
callback. This lambda is invoked continuously as the user drags the thumb, providing a Float
value from 0.0
(start) to 1.0
(end).
This is useful for creating complex animations synchronized with the slide gesture.
var slideProgress by remember { mutableFloatStateOf(0f) }
// You can use `slideProgress` to animate other elements on the screen.
Text(
text = "Progress: ${(slideProgress * 100).toInt()}%",
modifier = Modifier.alpha(slideProgress)
)
SlideToUnlock(
isSlided = isSlided,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
onSlideCompleted = { isSlided = true },
onSlideFractionChanged = { fraction ->
slideProgress = fraction
}
)
RevenueCat Integration
Slide to Unlock supports integrating features with RevenueCat SDK for your Android & Kotlin Multiplatform project, and you can simply implement "Slide to Purchases" or "Slide to Subscribe" it by adding the dependency below:
[versions]
#...
slidetounlockPurchases = "1.0.1"
[libraries]
#...
compose-slidetounlock-purchases = { module = "com.revenuecat.purchases:slide-to-unlock-purchases", version.ref = "slidetounlockPurchases" }
Gradle
Add the dependency below to your module's build.gradle.kts
file:
dependencies {
implementation("com.revenuecat.purchases:slide-to-unlock-purchases:$version")
// if you're using Version Catalog
implementation(libs.compose.slidetounlock.purchases)
}
For Kotlin Multiplatform, add the dependency below to your module's build.gradle.kts
file:
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.compose.slidetounlock.purchases)
}
}
}
SlideToPurchases for Jetpack Compose
It's very similar to use like SlideToUnlock
composable function, but it contains a few more parameters as required to purchases in app product with RevenueCat SDK.
SlideToPurchases
is a customizable Jetpack Compose component that provides a sleek "slide-to-unlock" UI to initiate in-app purchases through the RevenueCat SDK. It's designed to be a drop-in replacement for a traditional purchase button, offering a more engaging and intentional user experience.
The library provides a set of overloaded SlideToPurchases
composables to handle various purchasing scenarios, including different product types, subscription upgrades, and promotional offers, all while abstracting away the underlying purchase logic.
Expectation
- Engaging UI: A "slide-to-unlock" gesture provides a clear and deliberate user action for making a purchase. You can highlight hint text and add dynamic behavior to boost user engagement.
- Seamless Integration: Directly integrates with RevenueCat's
Purchases.sharedInstance.awaitPurchase
methods. - State Handling: Automatically manages and reports the state of the purchase (
Loading
,Success
,Error
) through a simple callback. - Customizable: Offers easy customization for colors and hint texts to match your app's branding.
- Flexible: Basicailly supports all purchases methods by
StoreProduct
,Package
,SubscriptionOption
, and withPromotionalOffer
s andWinBackOffer
s.
To get started, simply add the SlideToPurchases
composable to your UI. The primary entry points require you to provide a purchaseable entity, such as a Package
or a StoreProduct
.
Basic Purchase with a Package
This is the most common use case. You fetch your offerings from RevenueCat and pass the desired Package
to the composable. The component will handle the rest.
// inside your coroutine scope
val currentOffering = Purchases.sharedInstance.awaitOfferings().current
val monthlyPackage = currentOffering?.getPackage("monthly")
if (monthlyPackage != null) {
var purchaseState by remember { mutableStateOf<PurchaseState?>(null) }
SlideToPurchases(
packageToPurchase = monthlyPackage,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
onPurchaseStateChanged = { newPurchaseState ->
purchaseState = newPurchaseState
// Handle state changes, e.g., show a dialog on success/error
}
)
// Optionally display the state
when (val state = purchaseState) {
is PurchaseState.Loading -> { /* Show a loading indicator */ }
is PurchaseState.Success -> { /* Navigate to a success screen */ }
is PurchaseState.Error -> { /* Show an error message */ }
null -> { /* Initial state */ }
}
}
The onPurchaseStateChanged
callback is the primary way to react to the outcome of the purchase flow. It provides a PurchaseState
sealed class, which can be one of three types:
PurchaseState.Loading
: Triggered when the slide is completed and the purchase process begins.PurchaseState.Success
: Triggered when the purchase is successful. It contains theSuccessfulPurchase
object from the RevenueCat SDK.PurchaseState.Error
: Triggered if the purchase fails or is cancelled by the user. It contains theException
.
The SlideToPurchases
composable is provided as a set of overloads, each tailored to a specific purchasing scenario.
Common Parameters
All SlideToPurchases
overloads share a set of common parameters for customization and event handling:
modifier: Modifier
: TheModifier
to be applied to the component.colors: SlideToUnlockColors
: An object to customize the component's appearance. UseDefaultSlideToUnlockColors()
to get started.hintTexts: HintTexts
: An object to customize the text displayed in the track. UseHintTexts.defaultHintTexts()
for default values.onSlideCompleted: () -> Unit
: A callback invoked immediately when the user completes the slide gesture, right before the purchase flow is initiated.onPurchaseStateChanged: (PurchaseState) -> Unit
: A callback that receives updates on the state of the purchase operation.
Overloads
1. Purchasing a StoreProduct
Use this overload for purchasing a standalone StoreProduct
. For subscriptions on Google Play, you can also manage upgrades.
@Composable
fun SlideToPurchases(
storeProduct: StoreProduct,
// ... common and platform-specific parameters
)
oldProductId: String?
: (Google Play only) The product ID of a subscription to upgrade from.replacementMode: GoogleReplacementMode
: (Google Play only) The proration mode for the upgrade.
2. Purchasing a Package
This is the recommended approach when using RevenueCat's Offerings system.
@Composable
fun SlideToPurchases(
packageToPurchase: Package,
// ... common and platform-specific parameters
)
3. Purchasing a SubscriptionOption
A Google Play-only overload for purchasing a specific subscription option, such as a particular plan and offer.
@Composable
fun SlideToPurchases(
subscriptionOption: SubscriptionOption,
// ... common and platform-specific parameters
)
4. Purchasing with a PromotionalOffer
An App Store-only overload for applying a signed promotional offer to a purchase.
@Composable
fun SlideToPurchases(
packageToPurchase: Package, // or storeProduct: StoreProduct
promotionalOffer: PromotionalOffer,
// ... common parameters
)
5. Purchasing with a WinBackOffer
An iOS-only overload (requires iOS 18+) for applying a win-back offer to a lapsed subscription.
@Composable
fun SlideToPurchases(
packageToPurchase: Package, // or storeProduct: StoreProduct
winBackOffer: WinBackOffer,
// ... common parameters
)
By providing these clear and distinct overloads, SlideToPurchaces
simplifies the process of integrating a beautiful and functional purchase UI into your Jetpack Compose app, allowing you to focus on your app's core logic while providing a great user experience.
Find this repository useful? 😻
Support it by joining stargazers for this repository. :star:
Also, follow the main contributor on GitHub for the next creations! 🤩
License
Copyright (c) 2025 RevenueCat, Inc.
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.