Drawer
An unstyled, draggable edge-attached panel for drawers, side sheets, and sheets.
View Markdownimport androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragHandle
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerDemo() {
val snapPoints = remember {
DrawerSnapPoints<DrawerDemoValue> {
DrawerDemoValue.Closed at DrawerSnapPoint.Zero
DrawerDemoValue.Open at DrawerSnapPoint.ContentSize
}
}
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerDemoValue.Open,
snapPoints = snapPoints,
)
}
Box(Modifier.fillMaxSize()) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.align(Alignment.Center)
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open drawer")
}
UnstyledDrawer(state = drawerState) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 24.dp, top = 12.dp, end = 24.dp, bottom = 24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DragHandle {
Box(
Modifier
.width(32.dp)
.height(4.dp)
.background(Theme[colors][contentToken]),
)
}
Text("Here is the content of the drawer.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
}
}
}The default demo shows a modal bottom drawer with a draggable handle.
Features
- Named snap points for closed, peek, and expanded panel states
- Start, end, top, and bottom placement
- Modal, overlay, and in-place presentation without prescribed visuals
- Gesture, outside-click, Back, Escape, and accessibility dismissal support
- Caller-controlled Android system-bar icon appearance while a drawer is presented
Installation
implementation("com.composables:composeunstyled-drawer:2.9.2")Composition
DrawerHost {
UnstyledDrawer {
Viewport {
Panel {
DragHandle()
}
}
SwipeArea()
}
}Concepts
UnstyledDrawerStatemaps your values to visible panel sizes and controls movement between them.DrawerSnapPointsassociates each state value withZero,ContentSize, or a custom visible size.UnstyledDrawerselects the placement, presentation, dismissal behavior, and caller-owned overlay.Viewportdefines the finite area used to measure the panel and resolve snap points.Panelis the draggable surface. Its modifier and content own all visual and size choices.DrawerHostprovides the portal layer required byDrawerPresentation.Overlay.
Accessibility
Use DragHandle when the drawer can move between snap points. It exposes expand, collapse, and
dismiss actions to assistive technology. Modal drawers block outside interaction while open and
expose outside dismissal when a Zero snap point and dismissOnClickOutside are present.
Give the panel content an appropriate accessible name and use semantic controls for its actions.
Choosing a presentation
Use Modal for a flow that stands apart from the current screen, such as a modal bottom sheet,
account picker, or confirmation panel. It renders in a modal layer and blocks interaction behind
the drawer. It does not need DrawerHost.
Use Overlay for a drawer that covers the app without taking control away from it, such as a
desktop inspector, navigation panel, or shortcut tray. It renders through DrawerHost and leaves
the rest of the app interactive. It is the only presentation that supports SwipeArea.
Use InPlace when the drawer belongs to one bounded part of the UI, such as a preview pane,
editor, or embedded workflow. It renders where you declare it without a modal layer or portal. Its
panel overlays its Viewport; it does not push sibling content.
Code Examples
Show and hide a drawer
Assign targetValue to animate to a supported value. A value mapped to DrawerSnapPoint.Zero
represents a fully closed drawer.
enum class DrawerValue { Closed, Open }
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerValue.Closed,
snapPoints = DrawerSnapPoints {
DrawerValue.Closed at DrawerSnapPoint.Zero
DrawerValue.Open at DrawerSnapPoint.ContentSize
},
)
}
UnstyledButton(
onClick = { drawerState.targetValue = DrawerValue.Open },
modifier = Modifier
.background(Color.White)
.border(1.dp, Color.Black),
) {
BasicText("Open drawer")
}
UnstyledDrawer(state = drawerState) {
Viewport {
Panel(
modifier = Modifier
.background(Color.White)
.border(1.dp, Color.Black),
) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerValue.Closed },
modifier = Modifier
.background(Color.White)
.border(1.dp, Color.Black),
) {
BasicText("Close drawer")
}
}
}
}Add a peek state
Use a percentage snap point to keep part of the drawer visible.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragHandle
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerPeekDemoValue {
Closed,
Peek,
Expanded,
}
@Preview
@Composable
fun DrawerPeekDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerPeekDemoValue.Peek,
snapPoints = DrawerSnapPoints {
DrawerPeekDemoValue.Closed at DrawerSnapPoint.Zero
DrawerPeekDemoValue.Peek at DrawerSnapPoint { viewportSize, _ -> viewportSize * 0.2f }
DrawerPeekDemoValue.Expanded at DrawerSnapPoint.ContentSize
},
)
}
Box(Modifier.fillMaxSize()) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerPeekDemoValue.Peek },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.align(Alignment.Center)
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Show drawer")
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 24.dp, top = 12.dp, end = 24.dp, bottom = 24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DragHandle {
Box(
Modifier
.width(32.dp)
.height(4.dp)
.background(Theme[colors][contentToken]),
)
}
Text("Here is the content of the drawer.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerPeekDemoValue.Expanded },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Expand")
}
UnstyledButton(
onClick = { drawerState.targetValue = DrawerPeekDemoValue.Peek },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Peek")
}
}
}
}
}
}
}Set a custom snap point
This drawer’s middle state uses half of the viewport height.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerCustomSnapPointDemoValue {
Closed,
Half,
Open,
}
@Preview
@Composable
fun DrawerCustomSnapPointDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerCustomSnapPointDemoValue.Half,
snapPoints = DrawerSnapPoints {
DrawerCustomSnapPointDemoValue.Closed at DrawerSnapPoint.Zero
DrawerCustomSnapPointDemoValue.Half at DrawerSnapPoint { viewportSize, _ ->
viewportSize * 0.5f
}
DrawerCustomSnapPointDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text("The half state uses half of the viewport height.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerCustomSnapPointDemoValue.Half },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Show half")
}
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text("Here is the content of the drawer.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerCustomSnapPointDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open fully")
}
UnstyledButton(
onClick = { drawerState.targetValue = DrawerCustomSnapPointDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
}
}
}Build a side sheet
Set the placement to Start and give the panel a fixed width.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DrawerHost
import com.composeunstyled.DrawerPlacement
import com.composeunstyled.DrawerPresentation
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.SwipeArea
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.inputBackgroundToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerSideSheetDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerSideSheetDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerSideSheetDemoValue.Open,
snapPoints = DrawerSnapPoints {
DrawerSideSheetDemoValue.Closed at DrawerSnapPoint.Zero
DrawerSideSheetDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
DrawerHost(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize()) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerSideSheetDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.align(Alignment.Center)
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open side sheet")
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
placement = DrawerPlacement.Start,
presentation = DrawerPresentation.Overlay,
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.width(288.dp)
.fillMaxHeight()
.background(Theme[colors][inputBackgroundToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 12.dp, top = 24.dp, end = 24.dp, bottom = 24.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Here is the content of the drawer.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerSideSheetDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
SwipeArea(
modifier = Modifier
.width(24.dp)
.fillMaxHeight(),
)
}
}
}
}Open from the screen edge
Add SwipeArea to an overlay drawer to open it from the outlined edge target.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DrawerHost
import com.composeunstyled.DrawerPlacement
import com.composeunstyled.DrawerPresentation
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.SwipeArea
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.inputBackgroundToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerSwipeAreaDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerSwipeAreaDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerSwipeAreaDemoValue.Closed,
snapPoints = DrawerSnapPoints {
DrawerSwipeAreaDemoValue.Closed at DrawerSnapPoint.Zero
DrawerSwipeAreaDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
DrawerHost(Modifier.fillMaxSize()) {
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
placement = DrawerPlacement.Start,
presentation = DrawerPresentation.Overlay,
) {
Box(Modifier.fillMaxSize()) {
val swipeAreaModifier = Modifier
.align(Alignment.CenterStart)
.width(200.dp)
.fillMaxHeight()
Box(
modifier = swipeAreaModifier.border(1.dp, Theme[colors][borderToken]),
contentAlignment = Alignment.Center,
) {
Text(
"Start a swipe anywhere inside this area",
modifier = Modifier.padding(16.dp),
)
}
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.width(288.dp)
.fillMaxHeight()
.background(Theme[colors][inputBackgroundToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 12.dp, top = 24.dp, end = 24.dp, bottom = 24.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Here is the content of the drawer.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerSwipeAreaDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
SwipeArea(swipeAreaModifier)
}
}
}
}Add an overlay
Provide Overlay to render caller-owned content behind the drawer.
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragHandle
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Overlay
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.scrimToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerOverlayDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerOverlayDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerOverlayDemoValue.Open,
snapPoints = DrawerSnapPoints {
DrawerOverlayDemoValue.Closed at DrawerSnapPoint.Zero
DrawerOverlayDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
Box(Modifier.fillMaxSize()) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerOverlayDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.align(Alignment.Center)
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open drawer")
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
overlay = {
Overlay(
modifier = Modifier.fillMaxSize().background(
Theme[colors][scrimToken],
),
enter = fadeIn(),
exit = fadeOut(),
)
},
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 24.dp, top = 12.dp, end = 24.dp, bottom = 24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DragHandle {
Box(
Modifier
.width(32.dp)
.height(4.dp)
.background(Theme[colors][contentToken]),
)
}
Text("Here is the content of the drawer.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerOverlayDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
}
}
}Disable gestures
Set gesturesEnabled to false when the drawer should only move through app controls.
UnstyledDrawer(
state = drawerState,
gesturesEnabled = false,
) {
Viewport {
Panel {
Text("Drawer content")
}
}
}Disable outside click dismissal
Set dismissOnClickOutside to false when a click outside a modal drawer must not close it.
UnstyledDrawer(
state = drawerState,
dismissOnClickOutside = false,
) {
Viewport {
Panel {
Text("Drawer content")
}
}
}Disable Back and Escape dismissal
Set dismissOnNavigateBack to false when Back and Escape must not close the drawer.
UnstyledDrawer(
state = drawerState,
dismissOnNavigateBack = false,
) {
Viewport {
Panel {
Text("Drawer content")
}
}
}Prevent a state change
Return false from confirmValueChange to reject a transition, such as closing an unsaved form.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.Check
import com.composables.icons.lucide.Lucide
import com.composeunstyled.CheckedIndicator
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledCheckbox
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.UnstyledIcon
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerConfirmValueChangeDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerConfirmValueChangeDemo() {
var canClose by remember { mutableStateOf(false) }
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerConfirmValueChangeDemoValue.Open,
snapPoints = DrawerSnapPoints {
DrawerConfirmValueChangeDemoValue.Closed at DrawerSnapPoint.Zero
DrawerConfirmValueChangeDemoValue.Open at DrawerSnapPoint.ContentSize
},
confirmValueChange = { change ->
change.targetValue != DrawerConfirmValueChangeDemoValue.Closed || canClose
},
)
}
Box(Modifier.fillMaxSize()) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerConfirmValueChangeDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.align(Alignment.Center)
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open drawer")
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text("Here is the content of the drawer.")
Text(if (canClose) "Closing is allowed." else "Closing is blocked.")
UnstyledCheckbox(
checked = canClose,
onCheckedChange = { canClose = it },
modifier = Modifier.fillMaxWidth(),
accessibilityLabel = "Allow closing",
indication = LocalIndication.current,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
CheckedIndicator(
modifier = Modifier
.clip(RectangleShape)
.size(24.dp)
.background(Theme[colors][surfaceToken], RectangleShape)
.border(1.dp, Theme[colors][borderToken], RectangleShape),
indication = LocalIndication.current,
) {
UnstyledIcon(Lucide.Check)
}
Spacer(Modifier.width(12.dp))
Text("Allow closing")
}
}
UnstyledButton(
onClick = { drawerState.targetValue = DrawerConfirmValueChangeDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
}
}
}Control Android system-bar icons
Set systemUi to match the icon color needed by your drawer. The drawer applies the requested
appearance while it is presented and restores the previous appearance after it closes.
UnstyledDrawer(
state = drawerState,
placement = DrawerPlacement.Bottom,
presentation = DrawerPresentation.Modal,
systemUi = SystemUi(
statusBar = SystemUiAppearance.Dark,
navigationBar = SystemUiAppearance.Dark,
),
) {
Viewport {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.border(1.dp, Color.Black),
) {
BasicText("Drawer with dark system-bar icons")
}
}
}Working with the soft keyboard
Use the IME window inset in Viewport to keep the drawer above the soft keyboard.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.composeunstyled.DragHandle
import com.composeunstyled.DrawerPanelAlignment
import com.composeunstyled.DrawerPlacement
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.SystemUi
import com.composeunstyled.SystemUiAppearance
import com.composeunstyled.Text
import com.composeunstyled.TextInput
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.UnstyledTextField
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerImeDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerImeDemo() {
val snapPoints = remember {
DrawerSnapPoints<DrawerImeDemoValue> {
DrawerImeDemoValue.Closed at DrawerSnapPoint.Zero
DrawerImeDemoValue.Open at DrawerSnapPoint.ContentSize
}
}
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerImeDemoValue.Closed,
snapPoints = snapPoints,
)
}
val input = rememberTextFieldState()
val fieldTextStyle = TextStyle(fontSize = 16.sp, lineHeight = 24.sp)
Box(Modifier.fillMaxSize()) {
Box(
modifier = Modifier.fillMaxSize().padding(24.dp),
contentAlignment = Alignment.Center,
) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerImeDemoValue.Open },
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
modifier = Modifier.background(
Theme[colors][surfaceToken],
).border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open drawer")
}
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
placement = DrawerPlacement.Bottom,
systemUi = SystemUi(
statusBar = SystemUiAppearance.Light,
navigationBar = SystemUiAppearance.Dark,
),
) {
Box(Modifier.fillMaxSize()) {
Viewport(
modifier = Modifier.fillMaxSize(),
panelAlignment = DrawerPanelAlignment.Center,
windowInsets = WindowInsets.ime,
) {
Panel(
modifier = Modifier
.widthIn(max = 640.dp)
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
) {
Box {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(start = 24.dp, top = 60.dp, end = 24.dp, bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
"Here is the content of the drawer.",
fontSize = 24.sp,
lineHeight = 32.sp,
)
Text(
"Focus a field to test the keyboard inset.",
fontSize = 14.sp,
lineHeight = 20.sp,
)
UnstyledTextField(
state = input,
modifier = Modifier.fillMaxWidth(),
accessibilityLabel = "First field",
lineLimits = TextFieldLineLimits.SingleLine,
cursorBrush = SolidColor(Theme[colors][contentToken]),
textStyle = fieldTextStyle,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
TextInput(
modifier = Modifier
.fillMaxWidth()
.border(1.dp, Theme[colors][borderToken])
.padding(horizontal = 12.dp, vertical = 10.dp),
placeholder = {
Text("Type here", style = fieldTextStyle)
},
)
}
}
UnstyledButton(
onClick = { drawerState.targetValue = DrawerImeDemoValue.Closed },
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close drawer")
}
}
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.zIndex(1f)
.fillMaxWidth()
.padding(top = 12.dp),
contentAlignment = Alignment.TopCenter,
) {
DragHandle {
Box(Modifier.width(32.dp).height(4.dp).background(Theme[colors][contentToken]))
}
}
}
}
}
}
}
}
}Applying Overscroll Effect
Pass an OverscrollEffect to Panel to customize its motion beyond a snap point.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.spring
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
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.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.tooling.preview.Preview
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 com.composeunstyled.DragHandle
import com.composeunstyled.DrawerPlacement
import com.composeunstyled.DrawerPresentation
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
private enum class DrawerOverscrollDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerOverscrollDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerOverscrollDemoValue.Open,
snapPoints = DrawerSnapPoints {
DrawerOverscrollDemoValue.Closed at DrawerSnapPoint.Zero
DrawerOverscrollDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
val overscrollEffect = remember { ElasticOverscrollEffect() }
Box(Modifier.fillMaxSize()) {
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
placement = DrawerPlacement.Bottom,
presentation = DrawerPresentation.InPlace,
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 24.dp, top = 12.dp, end = 24.dp, bottom = 24.dp),
overscrollEffect = overscrollEffect,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DragHandle {
Box(
Modifier
.width(32.dp)
.height(4.dp)
.background(Theme[colors][contentToken]),
)
}
Text("Here is the content of the drawer.")
Text("Pull upward past the open limit")
}
}
}
}
}
}
private class ElasticOverscrollEffect : OverscrollEffect {
var offsetPx: Float by mutableFloatStateOf(0f)
private set
override fun applyToScroll(
delta: Offset,
source: androidx.compose.ui.input.nestedscroll.NestedScrollSource,
performScroll: (Offset) -> Offset,
): Offset {
val consumed = performScroll(delta)
val overscroll = delta - consumed
offsetPx += overscroll.y * ElasticOverscrollOffsetMultiplier
return consumed
}
override suspend fun applyToFling(
velocity: Velocity,
performFling: suspend (Velocity) -> Velocity,
) {
val releaseOffset = offsetPx
if (releaseOffset == 0f) {
performFling(velocity)
return
}
coroutineScope {
val rebound = launch {
animate(
initialValue = releaseOffset,
targetValue = 0f,
initialVelocity = velocity.y * ElasticOverscrollOffsetMultiplier,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium,
),
) { value, _ ->
offsetPx = value
}
}
try {
performFling(velocity)
} finally {
rebound.join()
}
}
}
override val isInProgress: Boolean
get() = offsetPx != 0f
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 offset = IntOffset(x = 0, y = offsetPx.roundToInt())
placeable.placeRelativeWithLayer(offset.x, offset.y)
}
}
}
}
private const val ElasticOverscrollOffsetMultiplier = 0.55fAdd spacing around a drawer
Set Viewport window insets to leave space around the drawer.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragHandle
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerSpacingDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerSpacingDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerSpacingDemoValue.Open,
snapPoints = DrawerSnapPoints {
DrawerSpacingDemoValue.Closed at DrawerSnapPoint.Zero
DrawerSpacingDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
val spacing = with(LocalDensity.current) { 24.dp.roundToPx() }
Box(Modifier.fillMaxSize()) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerSpacingDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.align(Alignment.Center)
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open drawer")
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
) {
Viewport(
modifier = Modifier.fillMaxSize(),
windowInsets = WindowInsets(
left = spacing,
right = spacing,
bottom = spacing,
),
) {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 24.dp, top = 12.dp, end = 24.dp, bottom = 24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DragHandle {
Box(
Modifier
.width(32.dp)
.height(4.dp)
.background(Theme[colors][contentToken]),
)
}
Text("Here is the content of the drawer.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerSpacingDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
}
}
}Handle dynamic content
Use DrawerSnapPoint.ContentSize to track a panel as its content changes.
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragHandle
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerDynamicContentDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerDynamicContentDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerDynamicContentDemoValue.Open,
snapPoints = DrawerSnapPoints {
DrawerDynamicContentDemoValue.Closed at DrawerSnapPoint.Zero
DrawerDynamicContentDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
var showDetails by remember { mutableStateOf(false) }
Box(Modifier.fillMaxSize()) {
UnstyledButton(
onClick = { drawerState.targetValue = DrawerDynamicContentDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.align(Alignment.Center)
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open drawer")
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 24.dp, top = 12.dp, end = 24.dp, bottom = 24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DragHandle {
Box(
Modifier
.width(32.dp)
.height(4.dp)
.background(Theme[colors][contentToken]),
)
}
Text("Here is the content of the drawer.")
if (showDetails) {
Text("Additional content.")
}
UnstyledButton(
onClick = { showDetails = showDetails.not() },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text(if (showDetails) "Remove content" else "Add content")
}
}
}
}
}
}
}Detect why a drawer was dismissed
Read the dismissal reason from the change passed to onDismissed.
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragHandle
import com.composeunstyled.DrawerSnapPoint
import com.composeunstyled.DrawerSnapPoints
import com.composeunstyled.DrawerValueChange
import com.composeunstyled.Panel
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDrawer
import com.composeunstyled.UnstyledDrawerState
import com.composeunstyled.Viewport
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme
private enum class DrawerDismissalReasonDemoValue {
Closed,
Open,
}
@Preview
@Composable
fun DrawerDismissalReasonDemo() {
val drawerState = remember {
UnstyledDrawerState(
initialValue = DrawerDismissalReasonDemoValue.Open,
snapPoints = DrawerSnapPoints {
DrawerDismissalReasonDemoValue.Closed at DrawerSnapPoint.Zero
DrawerDismissalReasonDemoValue.Open at DrawerSnapPoint.ContentSize
},
)
}
var dismissalReason by remember { mutableStateOf("No dismissal yet") }
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier.align(Alignment.Center).padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text("Dismissal reason: $dismissalReason")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerDismissalReasonDemoValue.Open },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Open drawer")
}
}
UnstyledDrawer(
state = drawerState,
modifier = Modifier.fillMaxSize(),
onDismissed = { dismissalReason = it.reason.label() },
) {
Viewport(Modifier.fillMaxSize()) {
Panel(
modifier = Modifier
.fillMaxWidth()
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken])
.padding(start = 24.dp, top = 12.dp, end = 24.dp, bottom = 24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DragHandle {
Box(
Modifier
.width(32.dp)
.height(4.dp)
.background(Theme[colors][contentToken]),
)
}
Text("Here is the content of the drawer.")
Text("Close with a gesture, outside click, Back, or Escape.")
UnstyledButton(
onClick = { drawerState.targetValue = DrawerDismissalReasonDemoValue.Closed },
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.background(Theme[colors][surfaceToken])
.border(1.dp, Theme[colors][borderToken]),
indication = LocalIndication.current,
) {
Text("Close")
}
}
}
}
}
}
}
private fun DrawerValueChange.Reason.label(): String = when (this) {
DrawerValueChange.Reason.Gesture -> "Gesture"
DrawerValueChange.Reason.NavigateBack -> "Navigate back"
DrawerValueChange.Reason.ClickOutside -> "Click outside"
DrawerValueChange.Reason.AccessibilityAction -> "Accessibility action"
DrawerValueChange.Reason.Programmatic -> "Programmatic"
else -> "Unknown"
}API Reference
UnstyledDrawer
| Parameter | Type | Description |
|---|---|---|
state |
UnstyledDrawerState<T> |
Controls the drawer’s current and target values. |
modifier |
Modifier |
Applied to the drawer’s top-level container. |
placement |
DrawerPlacement |
Edge from which the panel appears. |
presentation |
DrawerPresentation |
Whether the drawer is modal, overlaid through a portal, or rendered in place. |
gesturesEnabled |
Boolean |
Whether users can drag the panel. |
dismissOnNavigateBack |
Boolean |
Whether Back and Escape dismiss a visible drawer. |
dismissOnClickOutside |
Boolean |
Whether outside interaction dismisses a modal drawer. |
onDismissed |
(change: DrawerValueChange<T>) -> Unit |
Called after the drawer settles at its zero snap point. |
overlay |
(DrawerOverlayScope<T>.() -> Unit)? |
Caller-owned content rendered behind the panel. |
systemUi |
SystemUi |
System UI behavior while the drawer is presented. |
content |
DrawerScope.() -> Unit |
Drawer slots, including DrawerScope.Viewport and DrawerScope.SwipeArea. |
DrawerSnapPoint
| Parameter | Type | Description |
|---|---|---|
calculate |
(viewportSize: Dp, contentSize: Dp) -> Dp |
DrawerHost
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Applied to the host container. |
content |
() -> Unit |
Content that can contain overlay drawers. |
DrawerScope.SwipeArea
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Applied to the swipe area. |
DrawerScope.Viewport
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Applied to the viewport. |
panelAlignment |
DrawerPanelAlignment |
Cross-axis alignment for the panel. |
windowInsets |
WindowInsets |
Insets excluded from the available viewport. |
content |
DrawerViewportScope<T>.() -> Unit |
The panel slot. |
DrawerViewportScope.Panel
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Applied to the panel. |
overscrollEffect |
OverscrollEffect? |
Optional overscroll behavior for panel dragging. |
content |
DrawerPanelScope<T>.() -> Unit |
The panel content and DrawerPanelScope.DragHandle slot. |
DrawerPanelScope.DragHandle
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Applied to the handle. |
content |
() -> Unit |
Caller-owned handle content. |
DrawerOverlayScope.Overlay
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Applied to the overlay. |
enter |
EnterTransition |
Transition used when the overlay appears. |
exit |
ExitTransition |
Transition used when the overlay disappears. |
content |
() -> Unit |
Caller-owned overlay content. |
SystemUi
Controls the Android system-bar icon appearance while a drawer is presented.
| Parameter | Type | Description |
|---|---|---|
statusBar |
SystemUiAppearance |
The requested status-bar icon appearance. |
navigationBar |
SystemUiAppearance |
The requested navigation-bar icon appearance. |