---
title: Drawer
description: An unstyled, draggable edge-attached panel for drawers, side sheets, and sheets.
---



```kotlin expandable title="DrawerDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerDemo.kt"
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 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

```kotlin
implementation("com.composables:composeunstyled-drawer:2.10.0")
```

## Composition

```kotlin
DrawerHost {
  UnstyledDrawer {
    Viewport {
      Panel {
        DragHandle()
      }
    }

    SwipeArea()
  }
}
```

## Concepts

- `UnstyledDrawerState` maps your values to visible panel sizes and controls movement between them.
- `DrawerSnapPoints` associates each state value with `Zero`, `ContentSize`, or a custom visible size.
- `UnstyledDrawer` selects the placement, presentation, dismissal behavior, and caller-owned overlay.
- `Viewport` defines the finite area used to measure the panel and resolve snap points.
- `Panel` is the draggable surface. Its modifier and content own all visual and size choices.
- `DrawerHost` provides the portal layer required by `DrawerPresentation.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.

```kotlin expandable
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.



```kotlin expandable title="DrawerPeekDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerPeekDemo.kt"
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.



```kotlin expandable title="DrawerCustomSnapPointDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerCustomSnapPointDemo.kt"
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.



```kotlin expandable title="DrawerSideSheetDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerSideSheetDemo.kt"
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.



```kotlin expandable title="DrawerSwipeAreaDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerSwipeAreaDemo.kt"
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.



```kotlin expandable title="DrawerOverlayDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerOverlayDemo.kt"
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.

```kotlin expandable
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.

```kotlin expandable
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.

```kotlin expandable
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.



```kotlin expandable title="DrawerConfirmValueChangeDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerConfirmValueChangeDemo.kt"
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.

```kotlin expandable
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.



```kotlin expandable title="DrawerImeDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerImeDemo.kt"
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.



```kotlin expandable title="DrawerOverscrollDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerOverscrollDemo.kt"
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.55f
```



### Add spacing around a drawer

Set `Viewport` window insets to leave space around the drawer.



```kotlin expandable title="DrawerSpacingDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerSpacingDemo.kt"
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.



```kotlin expandable title="DrawerDynamicContentDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerDynamicContentDemo.kt"
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`.



```kotlin expandable title="DrawerDismissalReasonDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/drawer/DrawerDismissalReasonDemo.kt"
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. |
