Use scrollbars when content containers need clearer scroll affordances.
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.composables.ui.components.Text
import com.composables.ui.components.VerticalScrollbar
import com.composables.ui.components.rememberVerticalScrollbarState
import com.composables.ui.theme.colors
import com.composables.ui.theme.controlColor
import com.composeunstyled.theme.Theme
@Composable
fun ScrollbarExample() {
val scrollState = rememberScrollState()
val scrollbarState = rememberVerticalScrollbarState(scrollState)
Box(
modifier = Modifier
.height(220.dp)
.widthIn(max = 340.dp)
.clip(RoundedCornerShape(8.dp))
.background(Theme[colors][controlColor]),
) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
repeat(16) { index ->
Text("Scrollable row ${index + 1}")
}
}
VerticalScrollbar(
state = scrollbarState,
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight(),
)
}
}Installation
implementation("com.composables:ui:0.1.0")Add the required dependencies
implementation("com.composables:composeunstyled:2.7.0")
Copy and paste the following sources into your project
components/Scrollbars.kt
package com.composables.ui.components
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import com.composables.ui.theme.colors
import com.composables.ui.theme.mutedColor
import com.composeunstyled.Thumb
import com.composeunstyled.ThumbVisibility
import com.composeunstyled.UnstyledHorizontalScrollbar
import com.composeunstyled.UnstyledVerticalScrollbar
import com.composeunstyled.theme.Theme
import com.composeunstyled.ScrollbarState as UnstyledScrollbarState
import com.composeunstyled.rememberScrollbarState as rememberUnstyledScrollbarState
import kotlin.time.Duration.Companion.milliseconds
@Stable
class ScrollbarState internal constructor(
internal val unstyledState: UnstyledScrollbarState,
)
/**
* Creates and remembers scrollbar state for a scrollable container.
* @param scrollState ScrollState used to drive the scrollbar.
*/
@Composable
fun rememberVerticalScrollbarState(scrollState: ScrollState): ScrollbarState {
return rememberScrollbarState(rememberUnstyledScrollbarState(scrollState))
}
/**
* Creates and remembers scrollbar state for a scrollable container.
* @param lazyListState LazyListState used to drive the scrollbar.
*/
@Composable
fun rememberVerticalScrollbarState(lazyListState: LazyListState): ScrollbarState {
return rememberScrollbarState(rememberUnstyledScrollbarState(lazyListState))
}
/**
* Creates and remembers scrollbar state for a scrollable container.
* @param lazyGridState LazyGridState used to drive the scrollbar.
*/
@Composable
fun rememberVerticalScrollbarState(lazyGridState: LazyGridState): ScrollbarState {
return rememberScrollbarState(rememberUnstyledScrollbarState(lazyGridState))
}
@Composable
private fun rememberScrollbarState(unstyledState: UnstyledScrollbarState): ScrollbarState {
return remember(unstyledState) { ScrollbarState(unstyledState) }
}
/**
* A vertical scrollbar tied to a ScrollbarState.
* @param state Scrollbar state used by the scrollbar component.
* @param modifier Modifier applied to the scrollbar.
* @param enabled Whether the scrollbar can be interacted with.
* @param reverseLayout Whether the underlying layout scroll direction is reversed.
* @param interactionSource Interaction source used for dragging and hover state.
* @param thumbColor Color used for the scrollbar thumb.
* @param thumbShape Shape used for the scrollbar thumb.
* @param autoHide Whether the scrollbar thumb fades out while idle.
*/
@Composable
fun VerticalScrollbar(
state: ScrollbarState,
modifier: Modifier = Modifier,
enabled: Boolean = true,
reverseLayout: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
thumbColor: Color = Theme[colors][mutedColor],
thumbShape: Shape = RoundedCornerShape(999.dp),
autoHide: Boolean = true,
) {
UnstyledVerticalScrollbar(
scrollbarState = state.unstyledState,
modifier = modifier.width(10.dp),
enabled = enabled,
interactionSource = interactionSource,
reverseLayout = reverseLayout
) {
Thumb(
thumbVisibility = scrollbarThumbVisibility(autoHide),
enabled = enabled,
modifier = Modifier.clip(thumbShape).background(thumbColor.copy(alpha = 0.5f), thumbShape).width(6.dp),
)
}
}
/**
* A horizontal scrollbar tied to a ScrollbarState.
* @param state Scrollbar state used by the scrollbar component.
* @param modifier Modifier applied to the scrollbar.
* @param enabled Whether the scrollbar can be interacted with.
* @param reverseLayout Whether the underlying layout scroll direction is reversed.
* @param interactionSource Interaction source used for dragging and hover state.
* @param thumbColor Color used for the scrollbar thumb.
* @param thumbShape Shape used for the scrollbar thumb.
* @param autoHide Whether the scrollbar thumb fades out while idle.
*/
@Composable
fun HorizontalScrollbar(
state: ScrollbarState,
modifier: Modifier = Modifier,
enabled: Boolean = true,
reverseLayout: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
thumbColor: Color = Theme[colors][mutedColor],
thumbShape: Shape = RoundedCornerShape(999.dp),
autoHide: Boolean = true,
) {
UnstyledHorizontalScrollbar(state.unstyledState, modifier.height(10.dp), enabled, interactionSource, reverseLayout) {
Thumb(
thumbVisibility = scrollbarThumbVisibility(autoHide),
enabled = enabled,
modifier = Modifier.clip(thumbShape).background(thumbColor.copy(alpha = 0.45f), thumbShape).height(6.dp),
)
}
}
private fun scrollbarThumbVisibility(autoHide: Boolean): ThumbVisibility {
return if (autoHide) {
ThumbVisibility.HideWhileIdle(fadeIn(), fadeOut(), 700.milliseconds)
} else {
ThumbVisibility.AlwaysVisible
}
}components/Utils.kt
package com.composables.ui.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.composables.ui.theme.colors
import com.composables.ui.theme.ringColor
import com.composeunstyled.FocusRingVisibility
import com.composeunstyled.collectIsFocusVisibleAsState
import com.composeunstyled.outline
import com.composeunstyled.theme.Theme
@Composable
fun Modifier.focusRing(
interactionSource: InteractionSource,
width: Dp = 2.dp,
color: Color = Theme[colors][ringColor],
shape: Shape = RectangleShape,
offset: Dp = 0.dp,
visibility: FocusRingVisibility = FocusRingVisibility.FocusVisible,
): Modifier {
val showFocusRing by if (visibility == FocusRingVisibility.FocusVisible) {
interactionSource.collectIsFocusVisibleAsState()
} else {
interactionSource.collectIsFocusedAsState()
}
val animatedWidth by animateDpAsState(
targetValue = if (showFocusRing) width else 0.dp,
animationSpec = tween(durationMillis = 120),
label = "FocusRingWidth",
)
return this then Modifier.outline(
width = animatedWidth,
color = color,
shape = shape,
offset = offset,
)
}
@Composable
fun Modifier.bouncyPress(
interactionSource: InteractionSource,
enabled: Boolean = true,
pressedScale: Float = 0.98f,
): Modifier {
val pressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (enabled && pressed) pressedScale else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow,
),
label = "BouncyPressScale",
)
return this then Modifier.graphicsLayer {
scaleX = scale
scaleY = scale
}
}Examples
Horizontal
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.composables.ui.components.HorizontalScrollbar
import com.composables.ui.components.Text
import com.composables.ui.components.rememberVerticalScrollbarState
import com.composables.ui.theme.colors
import com.composables.ui.theme.controlColor
import com.composables.ui.theme.panelColor
import com.composeunstyled.theme.Theme
@Composable
fun HorizontalScrollbarExample() {
val scrollState = rememberScrollState()
val scrollbarState = rememberVerticalScrollbarState(scrollState)
Box(
modifier = Modifier
.height(88.dp)
.widthIn(max = 340.dp)
.clip(RoundedCornerShape(8.dp))
.background(Theme[colors][controlColor]),
) {
Row(
modifier = Modifier
.horizontalScroll(scrollState)
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
repeat(10) { index ->
Box(
modifier = Modifier
.size(width = 96.dp, height = 40.dp)
.clip(RoundedCornerShape(6.dp))
.background(Theme[colors][panelColor]),
contentAlignment = Alignment.Center,
) {
Text("Item ${index + 1}")
}
}
}
HorizontalScrollbar(
state = scrollbarState,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
)
}
}LazyColumn
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.composables.ui.components.Text
import com.composables.ui.components.VerticalScrollbar
import com.composables.ui.components.rememberVerticalScrollbarState
import com.composables.ui.theme.colors
import com.composables.ui.theme.controlColor
import com.composeunstyled.theme.Theme
@Composable
fun LazyColumnScrollbarExample() {
val lazyListState = rememberLazyListState()
val scrollbarState = rememberVerticalScrollbarState(lazyListState)
Box(
modifier = Modifier
.height(132.dp)
.width(340.dp)
.clip(RoundedCornerShape(8.dp))
.background(Theme[colors][controlColor]),
) {
LazyColumn(
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(10.dp),
contentPadding = PaddingValues(16.dp),
modifier = Modifier.fillMaxWidth()
) {
items(200) { index ->
Text("Build artifact ${index + 1}")
}
}
VerticalScrollbar(
state = scrollbarState,
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight(),
)
}
}Always visible
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.composables.ui.components.Text
import com.composables.ui.components.VerticalScrollbar
import com.composables.ui.components.rememberVerticalScrollbarState
import com.composables.ui.theme.colors
import com.composables.ui.theme.controlColor
import com.composeunstyled.theme.Theme
@Composable
fun AlwaysVisibleScrollbarExample() {
val scrollState = rememberScrollState()
val scrollbarState = rememberVerticalScrollbarState(scrollState)
Box(
modifier = Modifier
.height(132.dp)
.widthIn(max = 340.dp)
.clip(RoundedCornerShape(8.dp))
.background(Theme[colors][controlColor]),
) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
repeat(24) { index ->
Text("Always visible row ${index + 1}")
}
}
VerticalScrollbar(
state = scrollbarState,
autoHide = false,
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight(),
)
}
}Disabled
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.composables.ui.components.Text
import com.composables.ui.components.VerticalScrollbar
import com.composables.ui.components.rememberVerticalScrollbarState
import com.composables.ui.theme.colors
import com.composables.ui.theme.controlColor
import com.composeunstyled.theme.Theme
@Composable
fun DisabledScrollbarExample() {
val scrollState = rememberScrollState()
val scrollbarState = rememberVerticalScrollbarState(scrollState)
Box(
modifier = Modifier
.height(220.dp)
.widthIn(max = 340.dp)
.clip(RoundedCornerShape(8.dp))
.background(Theme[colors][controlColor]),
) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
repeat(16) { index ->
Text("Scrollable row ${index + 1}")
}
}
VerticalScrollbar(
state = scrollbarState,
enabled = false,
autoHide = false,
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight(),
)
}
}Horizontal disabled
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.composables.ui.components.HorizontalScrollbar
import com.composables.ui.components.Text
import com.composables.ui.components.rememberVerticalScrollbarState
import com.composables.ui.theme.colors
import com.composables.ui.theme.controlColor
import com.composables.ui.theme.panelColor
import com.composeunstyled.theme.Theme
@Composable
fun DisabledHorizontalScrollbarExample() {
val scrollState = rememberScrollState()
val scrollbarState = rememberVerticalScrollbarState(scrollState)
Box(
modifier = Modifier
.height(88.dp)
.widthIn(max = 340.dp)
.clip(RoundedCornerShape(8.dp))
.background(Theme[colors][controlColor]),
) {
Row(
modifier = Modifier
.horizontalScroll(scrollState)
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
repeat(10) { index ->
Box(
modifier = Modifier
.size(width = 96.dp, height = 40.dp)
.clip(RoundedCornerShape(6.dp))
.background(Theme[colors][panelColor]),
contentAlignment = Alignment.Center,
) {
Text("Item ${index + 1}")
}
}
}
HorizontalScrollbar(
state = scrollbarState,
enabled = false,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
)
}
}API Reference
rememberVerticalScrollbarState
Creates and remembers scrollbar state for a scrollable container.
@Composable
fun rememberVerticalScrollbarState(scrollState: ScrollState): ScrollbarState
| Parameter | Type | Description |
|---|---|---|
scrollState |
ScrollState |
ScrollState used to drive the scrollbar. |
rememberVerticalScrollbarState
Creates and remembers scrollbar state for a scrollable container.
@Composable
fun rememberVerticalScrollbarState(lazyListState: LazyListState): ScrollbarState
| Parameter | Type | Description |
|---|---|---|
lazyListState |
LazyListState |
LazyListState used to drive the scrollbar. |
rememberVerticalScrollbarState
Creates and remembers scrollbar state for a scrollable container.
@Composable
fun rememberVerticalScrollbarState(lazyGridState: LazyGridState): ScrollbarState
| Parameter | Type | Description |
|---|---|---|
lazyGridState |
LazyGridState |
LazyGridState used to drive the scrollbar. |
VerticalScrollbar
A vertical scrollbar tied to a ScrollbarState.
@Composable
fun VerticalScrollbar(
state: ScrollbarState,
modifier: Modifier = Modifier,
enabled: Boolean = true,
reverseLayout: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
thumbColor: Color = Theme[colors][mutedColor],
thumbShape: Shape = RoundedCornerShape(999.dp),
autoHide: Boolean = true,
)
| Parameter | Type | Description |
|---|---|---|
state |
ScrollbarState |
Scrollbar state used by the scrollbar component. |
modifier |
Modifier |
Modifier applied to the scrollbar. |
enabled |
Boolean |
Whether the scrollbar can be interacted with. |
reverseLayout |
Boolean |
Whether the underlying layout scroll direction is reversed. |
interactionSource |
MutableInteractionSource |
Interaction source used for dragging and hover state. |
thumbColor |
Color |
Color used for the scrollbar thumb. |
thumbShape |
Shape |
Shape used for the scrollbar thumb. |
autoHide |
Boolean |
Whether the scrollbar thumb fades out while idle. |
HorizontalScrollbar
A horizontal scrollbar tied to a ScrollbarState.
@Composable
fun HorizontalScrollbar(
state: ScrollbarState,
modifier: Modifier = Modifier,
enabled: Boolean = true,
reverseLayout: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
thumbColor: Color = Theme[colors][mutedColor],
thumbShape: Shape = RoundedCornerShape(999.dp),
autoHide: Boolean = true,
)
| Parameter | Type | Description |
|---|---|---|
state |
ScrollbarState |
Scrollbar state used by the scrollbar component. |
modifier |
Modifier |
Modifier applied to the scrollbar. |
enabled |
Boolean |
Whether the scrollbar can be interacted with. |
reverseLayout |
Boolean |
Whether the underlying layout scroll direction is reversed. |
interactionSource |
MutableInteractionSource |
Interaction source used for dragging and hover state. |
thumbColor |
Color |
Color used for the scrollbar thumb. |
thumbShape |
Shape |
Shape used for the scrollbar thumb. |
autoHide |
Boolean |
Whether the scrollbar thumb fades out while idle. |