The libraries available in Jetpack Compose for basic Material Design primitives offer a whole range of useful components. Just like the view system’s DrawerLayout, Compose offers its equivalent ModalDrawer, which can be used like so:

val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalDrawer(
    drawerState = drawerState,
    drawerContent = {
    // Drawer content here
    }
) {
    // Drawer host content here
}

This gives you a nice and easy navigation drawer layout, allowing you to build a modal that can be swiped out of the way or into view using gestures. It’s a common pattern for apps that feature a profile page with additional menu options, and so on. It also reduces dozens of lines of code and multiple files in the view system to just a handful using a declarative UI paradigm. And using the drawerState handle, it’s possible to trigger the drawer open/close animation programmatically as well.

The issue with the Material library’s implementation of this is that it’s super inflexible. Using the out-of-the-box component, you’re limited to showing the drawer from the left-hand side of the screen. A project I’m working on features a settings icon on the right in landscape orientation, in just the right location for the user’s thumb to reach it in a hurry and dismiss quickly. The app is designed to be used with two hands, and so what I really wanted was to show the modal from the right-hand side of the screen so it would be easily dismissable. A workaround found on StackOverflow was to do use a CompositionLocal to override the composable’s layout direction:

CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
    ModalDrawer(
        content = {
            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
                content()
            }
        }
    )
}

Once this is done, you can see your drawer appear from the right, and all the relevant gestures work from that direction too. So far so good? Unfortunately, you’re not out of the woods yet. At the time of writing (version 1.3.0) you cannot control how large the drawer (and scrim) are on the screen. The padding at the end of the drawer is hardcoded at 56dp. What’s more, I couldn’t just copy the code and modify myself because of this line:

Box(
    Modifier.swipeable(
        state = drawerState.swipeableState, // swipeableState is internal in Drawer.kt 😡
        anchors = anchors,

This unfortunately means that you’ll have to copy and paste the code for DrawerState as well, and write your own rememberDrawerState helper function. But once you do, the results are great. As most of the composable’s content was within a BoxWithConstraints, I modified the ratio of drawer to host screen by simply changing the drawer’s end padding to be relative to the box’s maxWidth, allowing me to control how far across the screen the drawer appeared:

@Composable
fun CustomModalDrawer(
    modifier: Modifier = Modifier,
    drawerContent: @Composable () -> Unit,
    drawerState: CustomModalDrawerState = rememberCustomModalDrawerState(DrawerValue.Closed),
    gesturesEnabled: Boolean = true,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerDefaults.Elevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    scrimColor: Color = DrawerDefaults.scrimColor,
    content: @Composable () -> Unit
) {
    BoxWithConstraints(modifier.fillMaxSize()) {

        val scope = rememberCoroutineScope()
        val modalDrawerConstraints = constraints

        val scrimWidth = maxWidth * 0.7f // Ratio of scrim width to box max width

        if (!modalDrawerConstraints.hasBoundedWidth) {
            throw IllegalStateException("Drawer shouldn't have infinite width")
        }
        val isRtl = LocalLayoutDirection.current == Rtl
        val minValue = -modalDrawerConstraints.maxWidth.toFloat()
        val maxValue = 0f
        val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)

        Box(
            Modifier
                .swipeable(
                    state = drawerState.swipeableState,
                    anchors = anchors,
                    thresholds = { _, _ -> FractionalThreshold(0.5f) },
                    orientation = Orientation.Horizontal,
                    enabled = gesturesEnabled,
                    reverseDirection = isRtl,
                    velocityThreshold = DrawerVelocityThreshold,
                    resistance = null
                )
        ) {
            Box {
                content()
            }
            Scrim(
                open = drawerState.isOpen,
                onClose = { scope.launch { drawerState.close() } },
                fraction = {
                    calculateFraction(minValue, maxValue, drawerState.offset.value)
                },
                color = scrimColor
            )
            Surface(
                modifier = with(LocalDensity.current) {
                    Modifier
                        .sizeIn(
                            minWidth = modalDrawerConstraints.minWidth.toDp(),
                            minHeight = modalDrawerConstraints.minHeight.toDp(),
                            maxWidth = modalDrawerConstraints.maxWidth.toDp(),
                            maxHeight = modalDrawerConstraints.maxHeight.toDp()
                        )
                }
                    .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
                    .padding(end = scrimWidth)
                    .semantics {
                        paneTitle = navigationMenuStr
                        if (drawerState.isOpen) {
                            dismiss {
                                scope.launch { drawerState.close() }
                                true
                            }
                        }
                    },
                shape = drawerShape,
                color = drawerBackgroundColor,
                contentColor = drawerContentColor,
                elevation = drawerElevation
            ) {
                drawerContent()
            }
        }
    }
}