# OverscrollEffect

Artifact: [androidx.compose.foundation:foundation](https://developer.android.com/jetpack/androidx/releases/compose-foundation) [View Source](https://cs.android.com/search?q=file:androidx/compose/foundation/Overscroll.kt+class:androidx.compose.foundation.OverscrollEffect) Added in [1.2.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.2.0) Common/AllAndroid/JVM
- Cmn

  ```
  interface OverscrollEffect
  ```

*** ** * ** ***

An OverscrollEffect represents a visual effect that displays when the edges of a scrolling container have been reached with a scroll or fling. To create an instance of the default / currently provided `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollFactory`, use `https://developer.android.com/reference/kotlin/androidx/compose/foundation/rememberOverscrollEffect.composable#rememberOverscrollEffect()`.

To implement, make sure to override `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#node()` - this has a default implementation for compatibility reasons, but is required for an OverscrollEffect to render.

OverscrollEffect conceptually 'decorates' scroll / fling events: consuming some of the delta or velocity before and/or after the event is consumed by the scrolling container. `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)` applies overscroll to a scroll event, and `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)` applies overscroll to a fling.

Higher level components such as `https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable#LazyColumn(androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.LazyListState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow,kotlin.Function1)` will automatically configure an OverscrollEffect for you. To use a custom OverscrollEffect you first need to provide it with scroll and/or fling events - usually by providing it to a `https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/scrollable.modifier#(androidx.compose.ui.Modifier).scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource)`. Then you can draw the effect on top of the scrolling content using `https://developer.android.com/reference/kotlin/androidx/compose/foundation/overscroll.modifier#(androidx.compose.ui.Modifier).overscroll(androidx.compose.foundation.OverscrollEffect)`.

```kotlin
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.overscroll
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// our custom offset overscroll that offset the element it is applied to when we hit the bound
// on the scrollable container.
class OffsetOverscrollEffect(val scope: CoroutineScope) : OverscrollEffect {
    private val overscrollOffset = Animatable(0f)

    override fun applyToScroll(
        delta: Offset,
        source: NestedScrollSource,
        performScroll: (Offset) -> Offset,
    ): Offset {
        // in pre scroll we relax the overscroll if needed
        // relaxation: when we are in progress of the overscroll and user scrolls in the
        // different direction = substract the overscroll first
        val sameDirection = sign(delta.y) == sign(overscrollOffset.value)
        val consumedByPreScroll =
            if (abs(overscrollOffset.value) > 0.5 && !sameDirection) {
                val prevOverscrollValue = overscrollOffset.value
                val newOverscrollValue = overscrollOffset.value + delta.y
                if (sign(prevOverscrollValue) != sign(newOverscrollValue)) {
                    // sign changed, coerce to start scrolling and exit
                    scope.launch { overscrollOffset.snapTo(0f) }
                    Offset(x = 0f, y = delta.y + prevOverscrollValue)
                } else {
                    scope.launch { overscrollOffset.snapTo(overscrollOffset.value + delta.y) }
                    delta.copy(x = 0f)
                }
            } else {
                Offset.Zero
            }
        val leftForScroll = delta - consumedByPreScroll
        val consumedByScroll = performScroll(leftForScroll)
        val overscrollDelta = leftForScroll - consumedByScroll
        // if it is a drag, not a fling, add the delta left to our over scroll value
        if (abs(overscrollDelta.y) > 0.5 && source == NestedScrollSource.UserInput) {
            scope.launch {
                // multiply by 0.1 for the sake of parallax effect
                overscrollOffset.snapTo(overscrollOffset.value + overscrollDelta.y * 0.1f)
            }
        }
        return consumedByPreScroll + consumedByScroll
    }

    override suspend fun applyToFling(
        velocity: Velocity,
        performFling: suspend (Velocity) -> Velocity,
    ) {
        val consumed = performFling(velocity)
        // when the fling happens - we just gradually animate our overscroll to 0
        val remaining = velocity - consumed
        overscrollOffset.animateTo(
            targetValue = 0f,
            initialVelocity = remaining.y,
            animationSpec = spring(),
        )
    }

    override val isInProgress: Boolean
        get() = overscrollOffset.value != 0f

    // Create a LayoutModifierNode that offsets by overscrollOffset.value
    override val node: DelegatableNode =
        object : Modifier.Node(), LayoutModifierNode {
            override fun MeasureScope.measure(
                measurable: Measurable,
                constraints: Constraints,
            ): MeasureResult {
                val placeable = measurable.measure(constraints)
                return layout(placeable.width, placeable.height) {
                    val offsetValue = IntOffset(x = 0, y = overscrollOffset.value.roundToInt())
                    placeable.placeRelativeWithLayer(offsetValue.x, offsetValue.y)
                }
            }
        }
}

val offset = remember { mutableStateOf(0f) }
val scope = rememberCoroutineScope()
// Create the overscroll controller
val overscroll = remember(scope) { OffsetOverscrollEffect(scope) }
// let's build a scrollable that scroll until -512 to 512
val scrollStateRange = (-512f).rangeTo(512f)
Box(
    Modifier.size(150.dp)
        .scrollable(
            orientation = Orientation.Vertical,
            state =
                rememberScrollableState { delta ->
                    // use the scroll data and indicate how much this element consumed.
                    val oldValue = offset.value
                    // coerce to our range
                    offset.value = (offset.value + delta).coerceIn(scrollStateRange)

                    offset.value - oldValue // indicate that we consumed what's needed
                },
            // pass the overscroll to the scrollable so the data is updated
            overscrollEffect = overscroll,
        )
        .background(Color.LightGray),
    contentAlignment = Alignment.Center,
) {
    Text(
        offset.value.roundToInt().toString(),
        style = TextStyle(fontSize = 32.sp),
        modifier =
            Modifier
                // show the overscroll only on the text, not the containers (just for fun)
                .overscroll(overscroll),
    )
}
```

## Summary

| ### Public functions |
|---|---|---|
| `suspend https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-unit/index.html` | `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)( velocity: https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity, performFling: suspend (https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity) -> https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity )` Applies overscroll to `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)`. | Cmn |
| `https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset` | `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)( delta: https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset, source: https://developer.android.com/reference/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollSource, performScroll: (https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset) -> https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset )` Applies overscroll to `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)`. | Cmn |

| ### Public properties |
|---|---|---|
| `open https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier` | `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#effectModifier()` **This property is deprecated.** This has been replaced with \`node\`. | Cmn |
| `https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-boolean/index.html` | `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#isInProgress()` Whether this OverscrollEffect is currently displaying overscroll. | Cmn |
| `open https://developer.android.com/reference/kotlin/androidx/compose/ui/node/DelegatableNode` | `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#node()` The `https://developer.android.com/reference/kotlin/androidx/compose/ui/node/DelegatableNode` that will render this OverscrollEffect and provide any required size or other information to this effect. | Cmn |

| ### Extension functions |
|---|---|---|
| `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` | `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect.https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#(androidx.compose.foundation.OverscrollEffect).withoutEventHandling()()` Returns a wrapped version of `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` that will not handle any incoming events. | Cmn |
| `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` | `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect.https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#(androidx.compose.foundation.OverscrollEffect).withoutVisualEffect()()` Returns a wrapped version of `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` with an empty `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#node()`. | Cmn |

## Public functions

### applyToFling

Cmn

```
suspend fun applyToFling(
    velocity: Velocity,
    performFling: suspend (Velocity) -> Velocity
): Unit
```

Applies overscroll to `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)`. `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)` should represent a fling (the release of a drag or scroll), and returns the amount of `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` consumed, so in simple cases the amount of overscroll to show should be equal to `velocity - performFling(velocity)`. The OverscrollEffect can optionally consume some `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` before calling `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)`, such as to release any existing tension. The implementation *must* call `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)` exactly once.

For example, assume we want to apply overscroll to a custom component that isn't using `https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/scrollable.modifier#(androidx.compose.ui.Modifier).scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource)`. Here is a simple example of a component using `https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/draggable.modifier#(androidx.compose.ui.Modifier).draggable(androidx.compose.foundation.gestures.DraggableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Boolean,kotlin.coroutines.SuspendFunction2,kotlin.coroutines.SuspendFunction2,kotlin.Boolean)` instead:

```kotlin
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

var dragPosition by remember { mutableStateOf(0f) }
val minPosition = -1000f
val maxPosition = 1000f

val draggableState = rememberDraggableState { delta ->
    val newPosition = (dragPosition + delta).coerceIn(minPosition, maxPosition)
    dragPosition = newPosition
}

Box(
    Modifier.size(100.dp).draggable(draggableState, orientation = Orientation.Horizontal),
    contentAlignment = Alignment.Center,
) {
    Text("Drag position $dragPosition")
}
```

To apply overscroll, we decorate the existing logic with applyToScroll, and return the amount of delta we have consumed when updating the drag position. We then call applyToFling using the velocity provided by onDragStopped.

```kotlin
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp

var dragPosition by remember { mutableStateOf(0f) }
val minPosition = -1000f
val maxPosition = 1000f

val overscrollEffect = rememberOverscrollEffect()

val draggableState = rememberDraggableState { delta ->
    // Horizontal, so convert the delta to a horizontal offset
    val deltaAsOffset = Offset(delta, 0f)

    val performDrag: (Offset) -> Offset = { remainingOffset ->
        val remainingDelta = remainingOffset.x
        val newPosition = (dragPosition + remainingDelta).coerceIn(minPosition, maxPosition)
        // Calculate how much delta we have consumed
        val consumed = newPosition - dragPosition
        dragPosition = newPosition
        // Return how much offset we consumed, so that we can show overscroll for what is left
        Offset(consumed, 0f)
    }

    if (overscrollEffect != null) {
        // Wrap the original logic inside applyToScroll
        overscrollEffect.applyToScroll(deltaAsOffset, NestedScrollSource.UserInput, performDrag)
    } else {
        performDrag(deltaAsOffset)
    }
}

Box(
    Modifier
        // Draw overscroll on the box
        .overscroll(overscrollEffect)
        .size(100.dp)
        .draggable(
            draggableState,
            orientation = Orientation.Horizontal,
            onDragStopped = {
                overscrollEffect?.applyToFling(Velocity(it, 0f)) { velocity ->
                    if (dragPosition == minPosition || dragPosition == maxPosition) {
                        // If we are at the min / max bound, give overscroll all of the velocity
                        Velocity.Zero
                    } else {
                        // If we aren't at the min / max bound, consume all of the velocity so
                        // overscroll won't show. Normally in this case something like
                        // Modifier.scrollable would use the velocity to update the scroll state
                        // with a fling animation, but just do nothing to keep this simpler.
                        velocity
                    }
                }
            },
        ),
    contentAlignment = Alignment.Center,
) {
    Text("Drag position $dragPosition")
}
```

| Parameters |
|---|---|
| `velocity: https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` | total `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` available |
| `performFling: suspend (https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity) -> https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` | the `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` consuming lambda that the overscroll is applied to. The `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` parameter represents how much `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` is available, and the return value is how much `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` was consumed. Any `https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Velocity` that was not consumed should be used to show the overscroll effect. |

### applyToScroll

Cmn

```
fun applyToScroll(
    delta: Offset,
    source: NestedScrollSource,
    performScroll: (Offset) -> Offset
): Offset
```

Applies overscroll to `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)`. `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)` should represent a drag / scroll, and returns the amount of delta consumed, so in simple cases the amount of overscroll to show should be equal to `delta - performScroll(delta)`. The OverscrollEffect can optionally consume some delta before calling `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)`, such as to release any existing tension. The implementation *must* call `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)` exactly once. This function should return the sum of all the delta that was consumed during this operation - both by the overscroll and `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)`.

For example, assume we want to apply overscroll to a custom component that isn't using `https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/scrollable.modifier#(androidx.compose.ui.Modifier).scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource)`. Here is a simple example of a component using `https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/draggable.modifier#(androidx.compose.ui.Modifier).draggable(androidx.compose.foundation.gestures.DraggableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Boolean,kotlin.coroutines.SuspendFunction2,kotlin.coroutines.SuspendFunction2,kotlin.Boolean)` instead:

```kotlin
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

var dragPosition by remember { mutableStateOf(0f) }
val minPosition = -1000f
val maxPosition = 1000f

val draggableState = rememberDraggableState { delta ->
    val newPosition = (dragPosition + delta).coerceIn(minPosition, maxPosition)
    dragPosition = newPosition
}

Box(
    Modifier.size(100.dp).draggable(draggableState, orientation = Orientation.Horizontal),
    contentAlignment = Alignment.Center,
) {
    Text("Drag position $dragPosition")
}
```

To apply overscroll, we need to decorate the existing logic with applyToScroll, and return the amount of delta we have consumed when updating the drag position. Note that we also need to call applyToFling - this is used as an end signal for overscroll so that effects can correctly reset after any animations, when the gesture has stopped.

```kotlin
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp

var dragPosition by remember { mutableStateOf(0f) }
val minPosition = -1000f
val maxPosition = 1000f

val overscrollEffect = rememberOverscrollEffect()

val draggableState = rememberDraggableState { delta ->
    // Horizontal, so convert the delta to a horizontal offset
    val deltaAsOffset = Offset(delta, 0f)

    val performDrag: (Offset) -> Offset = { remainingOffset ->
        val remainingDelta = remainingOffset.x
        val newPosition = (dragPosition + remainingDelta).coerceIn(minPosition, maxPosition)
        // Calculate how much delta we have consumed
        val consumed = newPosition - dragPosition
        dragPosition = newPosition
        // Return how much offset we consumed, so that we can show overscroll for what is left
        Offset(consumed, 0f)
    }

    if (overscrollEffect != null) {
        // Wrap the original logic inside applyToScroll
        overscrollEffect.applyToScroll(deltaAsOffset, NestedScrollSource.UserInput, performDrag)
    } else {
        performDrag(deltaAsOffset)
    }
}

Box(
    Modifier
        // Draw overscroll on the box
        .overscroll(overscrollEffect)
        .size(100.dp)
        .draggable(
            draggableState,
            orientation = Orientation.Horizontal,
            onDragStopped = {
                overscrollEffect?.applyToFling(Velocity(it, 0f)) { velocity ->
                    if (dragPosition == minPosition || dragPosition == maxPosition) {
                        // If we are at the min / max bound, give overscroll all of the velocity
                        Velocity.Zero
                    } else {
                        // If we aren't at the min / max bound, consume all of the velocity so
                        // overscroll won't show. Normally in this case something like
                        // Modifier.scrollable would use the velocity to update the scroll state
                        // with a fling animation, but just do nothing to keep this simpler.
                        velocity
                    }
                }
            },
        ),
    contentAlignment = Alignment.Center,
) {
    Text("Drag position $dragPosition")
}
```

| Parameters |
|---|---|
| `delta: https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset` | total scroll delta available |
| `source: https://developer.android.com/reference/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollSource` | the source of the delta |
| `performScroll: (https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset) -> https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset` | the scroll action that the overscroll is applied to. The `https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset` parameter represents how much delta is available, and the return value is how much delta was consumed. Any delta that was not consumed should be used to show the overscroll effect. |

| Returns |
|---|---|
| `https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Offset` | the delta consumed from `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)` by the operation of this function - including that consumed by `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)`. |

## Public properties

### effectModifier

Cmn Added in [1.2.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.2.0) Deprecated in [1.8.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.8.0)

```
open val effectModifier: Modifier
```

> [!CAUTION]
> **This property is deprecated.**   
> This has been replaced with \`node\`. If you are calling this property to render overscroll, use Modifier.overscroll() instead. If you are implementing OverscrollEffect, override \`node\` instead to render your overscroll, use this instead:
>
> ```kotlin
> Modifier.overscroll(this)
> ```

A `https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier` that will draw this OverscrollEffect

This API is deprecated- implementers should instead override `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#node()`. Callers should use `https://developer.android.com/reference/kotlin/androidx/compose/foundation/overscroll.modifier#(androidx.compose.ui.Modifier).overscroll(androidx.compose.foundation.OverscrollEffect)`.

### isInProgress

Cmn Added in [1.2.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.2.0)

```
val isInProgress: Boolean
```

Whether this OverscrollEffect is currently displaying overscroll.

| Returns |
|---|---|
| `https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-boolean/index.html` | true if this OverscrollEffect is currently displaying overscroll |

### node

Cmn Added in [1.8.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.8.0)

```
open val node: DelegatableNode
```

The `https://developer.android.com/reference/kotlin/androidx/compose/ui/node/DelegatableNode` that will render this OverscrollEffect and provide any required size or other information to this effect.

In most cases you should use `https://developer.android.com/reference/kotlin/androidx/compose/foundation/overscroll.modifier#(androidx.compose.ui.Modifier).overscroll(androidx.compose.foundation.OverscrollEffect)` to render this OverscrollEffect, which will internally attach this node to the hierarchy. The node should be attached before `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)` or `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)` is called to ensure correctness.

This property should return a single instance, and can only be attached once, as with other `https://developer.android.com/reference/kotlin/androidx/compose/ui/node/DelegatableNode`s.

## Extension functions

### OverscrollEffect.withoutEventHandling

Cmn Artifact: [androidx.compose.foundation:foundation](https://developer.android.com/jetpack/androidx/releases/compose-foundation) [View Source](https://cs.android.com/search?q=file:androidx/compose/foundation/Overscroll.kt+function:withoutEventHandling)

```
fun OverscrollEffect.withoutEventHandling(): OverscrollEffect
```

Returns a wrapped version of `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` that will not handle any incoming events. This means that calls to `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToScroll(androidx.compose.ui.geometry.Offset,androidx.compose.ui.input.nestedscroll.NestedScrollSource,kotlin.Function1)` / `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#applyToFling(androidx.compose.ui.unit.Velocity,kotlin.coroutines.SuspendFunction1)` will directly execute the provided performScroll / performFling lambdas, without the `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` ever seeing the incoming values. `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#node()` will still be attached, so that overscroll can render.

This can be useful if you want to render an `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` in a different component that normally provides events to overscroll, such as a `https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable#LazyColumn(androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.LazyListState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow,kotlin.Function1)`. Use this along with `https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#(androidx.compose.foundation.OverscrollEffect).withoutVisualEffect()` to create two wrapped instances: one that does not handle events, and one that does not draw, so you can ensure that the overscroll effect is only rendered once, and only receives events from one source.

| See also |
|---|---|
| `https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#(androidx.compose.foundation.OverscrollEffect).withoutVisualEffect()` |   |

### OverscrollEffect.withoutVisualEffect

Cmn Artifact: [androidx.compose.foundation:foundation](https://developer.android.com/jetpack/androidx/releases/compose-foundation) [View Source](https://cs.android.com/search?q=file:androidx/compose/foundation/Overscroll.kt+function:withoutVisualEffect)

```
fun OverscrollEffect.withoutVisualEffect(): OverscrollEffect
```

Returns a wrapped version of `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect` with an empty `https://developer.android.com/reference/kotlin/androidx/compose/foundation/OverscrollEffect#node()`. This prevents the overscroll effect from applying any visual effect, but it will still handle events.

This can be used along with `https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#(androidx.compose.foundation.OverscrollEffect).withoutEventHandling()` in cases where you wish to change where overscroll is rendered for a given component. Pass this wrapped instance that doesn't render to the component that handles events (such as `https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable#LazyColumn(androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.LazyListState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow,kotlin.Function1)`) to prevent it from drawing the overscroll effect. Then to separately render the original overscroll effect, you can directly pass it to `https://developer.android.com/reference/kotlin/androidx/compose/foundation/overscroll.modifier#(androidx.compose.ui.Modifier).overscroll(androidx.compose.foundation.OverscrollEffect)` (since that modifier only renders, and does not handle events). If instead you want to draw the overscroll in another component that handles events, such as a different lazy list, you need to first wrap the original overscroll effect with `https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#(androidx.compose.foundation.OverscrollEffect).withoutEventHandling()` to prevent it from also dispatching events.

```kotlin
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.foundation.withoutVisualEffect
import androidx.compose.material3.Text
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp

val items = remember { (1..100).toList() }
val state = rememberLazyListState()
val overscroll = rememberOverscrollEffect()
// Create a wrapped version of the above overscroll effect that does not apply a visual effect.
// This will be used inside LazyColumn to provide events to overscroll, without letting
// LazyColumn render the overscroll effect internally.
val overscrollWithoutVisualEffect = overscroll?.withoutVisualEffect()
LazyColumn(
    content = { items(items) { Text("Item $it") } },
    state = state,
    modifier =
        Modifier.size(300.dp)
            .clip(RectangleShape)
            // Manually render the overscroll on top of the lazy list _and_ the 'decorations' we
            // are
            // manually drawing, to make sure they will also be included in the overscroll
            // effect.
            .overscroll(overscroll)
            .drawBehind {
                state.layoutInfo.visibleItemsInfo.drop(1).forEach { info ->
                    val verticalOffset = info.offset.toFloat()
                    drawLine(
                        color = Color.Red,
                        start = Offset(0f, verticalOffset),
                        end = Offset(size.width, verticalOffset),
                    )
                }
            },
    // Pass the overscroll effect that does not apply a visual effect inside the LazyList to
    // receive overscroll events
    overscrollEffect = overscrollWithoutVisualEffect,
)
```

| See also |
|---|---|
| `https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#(androidx.compose.foundation.OverscrollEffect).withoutEventHandling()` |   |