suiss/uim · React Native Component
Drawer
A panel that slides in from any edge and can be swiped away, with snap points and a floating style.
Drawer is an open-source mobile component from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.


import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"
import { Text } from "@/components/ui/text"
function GoalContent() {
return (
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<View className="items-center gap-1 p-4">
<Text className="text-5xl font-semibold tracking-tight">350</Text>
<Text className="text-xs text-label-secondary uppercase">
Calories/day
</Text>
</View>
<DrawerFooter>
<Button>
<Text>Submit</Text>
</Button>
<DrawerClose render={<Button variant="secondary" />}>
<Text>Cancel</Text>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
)
}
export function DrawerOpenByDefaultExample() {
return (
<View className="items-start">
<Drawer defaultOpen showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
<Text>Open drawer</Text>
</DrawerTrigger>
<GoalContent />
</Drawer>
</View>
)
}import EsuissUI
import SwiftUI
struct DrawerOpenByDefaultExample: View {
var body: some View {
DrawerGoal(open: true)
}
}
struct DrawerGoal: View {
@State private var open: Bool
init(open: Bool = false) {
_open = State(initialValue: open)
}
var body: some View {
Button("Open drawer") { open = true }
.buttonStyle(.variant(.outline))
.drawer(isPresented: $open) {
DrawerGoalContent()
}
}
}
struct DrawerGoalContent: View {
var body: some View {
DrawerContent {
DrawerHeader {
DrawerTitle("Move goal")
DrawerDescription("Set your daily activity goal.")
}
VStack(spacing: 4) {
Text(verbatim: "350")
.font(.system(size: 48, weight: .semibold))
.tracking(-1.2)
.foregroundStyle(Color.label)
Text("Calories/day")
.textCase(.uppercase)
.textStyle(.xs)
.foregroundStyle(Color.labelSecondary)
}
.frame(maxWidth: .infinity)
.padding(16)
DrawerFooter {
Button {} label: {
Text("Submit").frame(maxWidth: .infinity)
}
.buttonStyle(.variant())
DrawerClose {
Text("Cancel").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.secondary))
}
}
}
}import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Drawer
@Composable
fun DrawerOpenByDefaultExample() {
DrawerGoal(defaultOpen = true, floating = false)
}
@Composable
fun DrawerGoal(defaultOpen: Boolean, floating: Boolean, label: String = "Open drawer") {
var open by rememberSaveable { mutableStateOf(defaultOpen) }
Column {
Button(label, onClick = { open = true }, variant = ButtonVariant.Outline)
Drawer(open = open, onOpenChange = { open = it }, floating = floating, showSwipeHandle = true) {
DrawerGoalContent(onClose = { open = false })
}
}
}Drawer presents content in a panel attached to an edge of the screen. It slides in from the bottom by default, or from the top, left or right with swipeDirection, and the user can drag it closed in that direction. The content renders through a portal (inside a full-window overlay on iOS) and respects the safe-area insets.
Set floating to inset the panel from the screen edges with all corners rounded. Use snapPoints to let a bottom drawer rest at several heights. Wrap your app in DrawerProvider and its screen in DrawerIndent to scale the page back behind an open bottom drawer.
Installation
$ pnpm dlx @esuiss/uim@latest add drawer$ pnpm dlx @esuiss/uim@latest add drawer --platform swift$ pnpm dlx @esuiss/uim@latest add drawer --platform kotlinThe CLI also adds what it builds on: Button, Icon, Text.
Manual
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-safe-area-context react-native-screens react-native-svg react-native-worklets-
Add the pieces it builds on: Button, Icon, Text, and the primitives
components/primitives/drawer.tsx,components/primitives/use-render.tsx. -
Copy the source into
components/ui/drawer.tsx:
import { cn } from "cn"
import { XIcon } from "phosphor-react-native"
import * as React from "react"
import {
Platform,
useWindowDimensions,
View,
type ViewProps,
type ViewStyle,
} from "react-native"
import {
FadeIn,
FadeOut,
ReduceMotion,
SlideInDown,
SlideInLeft,- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-safe-area-context react-native-screens react-native-svg react-native-worklets-
Add the pieces it builds on: Button, Icon, Text, and the primitives
components/primitives/drawer.tsx,components/primitives/use-render.tsx. -
Copy the source into
components/ui/drawer.tsx:
import { cn } from "cn"
import { XIcon } from "phosphor-react-native"
import * as React from "react"
import {
Platform,
useWindowDimensions,
View,
type ViewProps,
type ViewStyle,
} from "react-native"
import {
FadeIn,
FadeOut,
ReduceMotion,
SlideInDown,
SlideInLeft,- Update the import paths to match your project.
- Install the dependencies:
npx expo install phosphor-react-native react-native-reanimated react-native-safe-area-context react-native-screens react-native-svg react-native-worklets-
Add the pieces it builds on: Button, Icon, Text, and the primitives
components/primitives/drawer.tsx,components/primitives/use-render.tsx. -
Copy the source into
components/ui/drawer.tsx:
import { XIcon } from "phosphor-react-native"
import * as React from "react"
import {
Platform,
StyleSheet,
useWindowDimensions,
View,
type ViewProps,
type ViewStyle,
} from "react-native"
import {
FadeIn,
FadeOut,
ReduceMotion,
SlideInDown,
SlideInLeft,- Update the import paths to match your project.
-
Target iOS 17 or later.
-
Add the Swift package to your app:
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")- Copy the theme files every view uses:
import SwiftUI
import UIKit
struct RGBA: Sendable {
let red: CGFloat
let green: CGFloat
let blue: CGFloat
let alpha: CGFloat
init(_ hex: UInt32, _ alpha: CGFloat = 1) {
red = CGFloat((hex >> 16) & 0xFF) / 255
green = CGFloat((hex >> 8) & 0xFF) / 255
blue = CGFloat(hex & 0xFF) / 255
self.alpha = alpha
}import SwiftUI
extension Color {
public static let surface = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
public static let surfaceSecondary = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
public static let surfaceTertiary = Color.adaptive(light: .init(0xE8E8ED), dark: .init(0x2C2C2E))
public static let surfaceRaised = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
public static let label = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let labelSecondary = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
public static let labelTertiary = Color.adaptive(light: .init(0x86868B), dark: .init(0x86868B))
public static let labelQuaternary = Color.adaptive(light: .init(0xD2D2D7), dark: .init(0x424245))
public static let control = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
public static let controlPressed = Color.adaptive(light: .init(0x000000, 0.12), dark: .init(0xFFFFFF, 0.22))
public static let controlSelected = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let controlDisabled = Color.adaptive(light: .init(0x000000, 0.03), dark: .init(0xFFFFFF, 0.05))
public static let onControlSelected = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))- Copy the source and every file it uses:
import SwiftUI
extension View {
public func drawer<Content: View>(
isPresented: Binding<Bool>,
detents: Set<PresentationDetent>? = nil,
selection: Binding<PresentationDetent>? = nil,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> Content
) -> some View {
let binding = isPresented
return sheet(isPresented: isPresented, onDismiss: onDismiss) {
DrawerHost(detents: detents, selection: selection, content: content)
.environment(\.overlayDismiss, OverlayDismiss { binding.wrappedValue = false })
}
}import SwiftUI
public struct VariantButtonStyle: ButtonStyle {
public enum Variant: CaseIterable, Sendable {
case `default`, outline, secondary, tinted, ghost, plain, destructive, link
}
public enum Size: CaseIterable, Sendable {
case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl
var height: CGFloat {
switch self {
case .xs, .iconXs: 24
case .sm, .iconSm: 32
case .default, .icon: 36
case .lg, .iconLg: 44import SwiftUI
extension EnvironmentValues {
@Entry var buttonGroup: Axis? = nil
@Entry var buttonGroupSize: CGSize = .zero
}
enum ButtonGroupSpace: Hashable {
case group
}
struct ButtonGroupFrame {
let bounds: Anchor<CGRect>
let outlined: Bool
}import PhosphorSwift
import SwiftUI
extension Shadow {
static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}
struct SelectionIndicator<Mark: View>: View {
let checked: Bool
let pressed: Bool
let cornerRadius: CGFloat
let mark: Mark
@Environment(\.isEnabled) private var isEnabled
@Environment(\.isInvalid) private var invalidimport SwiftUI
extension View {
public func dialog<Content: View>(
isPresented: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
) -> some View {
modifier(
CoverPresenter(isPresented: isPresented) { visible in
CenteredStage(visible: visible, dismissible: true, content: content)
}
)
}
}
struct CenteredStage<Content: View>: View {import SwiftUI
extension EnvironmentValues {
@Entry public var isInvalid = false
@Entry var fieldInset = false
@Entry var fieldGroupInset = false
@Entry var fieldSetInset = false
}
extension View {
public func invalid(_ isInvalid: Bool = true) -> some View {
environment(\.isInvalid, isInvalid)
}
}
public enum FieldOrientation: CaseIterable, Sendable {import SwiftUI
extension EnvironmentValues {
@Entry public var hapticsEnabled = true
}
extension View {
public func haptics(_ enabled: Bool) -> some View {
environment(\.hapticsEnabled, enabled)
}
func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
}
func hapticFeedback<Trigger: Equatable>(import PhosphorSwift
import Observation
import SwiftUI
public enum PopupAlignment: Sendable, CaseIterable {
case start, center, end
}
struct OverlayDismiss: Sendable {
let action: @MainActor @Sendable () -> Void
@MainActor func callAsFunction() {
action()
}
}import SwiftUI
public struct Separator: View {
let orientation: Axis
public init(_ orientation: Axis = .horizontal) {
self.orientation = orientation
}
public var body: some View {
Rectangle()
.fill(Color.separator)
.frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
.frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
.accessibilityHidden(true)
}import SwiftUI
public enum TextVariant: CaseIterable, Sendable {
case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted
var style: TextStyle {
switch self {
case .h1: .xl3
case .h2: .xl2
case .h3: .xl
case .h4, .lead, .large: .lg
case .code, .small, .muted: .sm
default: .base
}
}- Add the files to your app target.
- Set
minSdkto 24 or higher and add the dependencies:
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")- Copy the theme files every view uses:
package com.esuiss.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffsetpackage com.esuiss.ui.theme
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Immutable
data class Colors(
val surface: Color,
val surfaceSecondary: Color,
val surfaceTertiary: Color,
val surfaceRaised: Color,
val label: Color,
val labelSecondary: Color,
val labelTertiary: Color,
val labelQuaternary: Color,- Copy the source and every file it uses:
package com.esuiss.ui.components
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Columnpackage com.esuiss.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShapepackage com.esuiss.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutablepackage com.esuiss.ui.components
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
val LocalContentStyle = compositionLocalOf { TextStyle.Default }
val LocalContentColor = compositionLocalOf { Color.Unspecified }
val LocalIconSize = compositionLocalOf { 20.dp }package com.esuiss.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme
@Composable
fun Icon(
imageVector: ImageVector,package com.esuiss.ui.components
import android.os.Build
import android.view.Window
import android.view.WindowManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideIn- Update the
com.esuiss.uipackage names to match your project.
Usage
import { DrawerProvider, DrawerIndentBackground, DrawerIndent, Drawer, DrawerOverlay, DrawerSwipeHandle, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription } from "@/components/ui/drawer"<View className="items-start">
<Drawer defaultOpen showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
<Text>Open drawer</Text>
</DrawerTrigger>
<GoalContent />
</Drawer>
</View>Compose DrawerTrigger and DrawerContent with a DrawerHeader (DrawerTitle, DrawerDescription), your content and a DrawerFooter. Use DrawerClose to close it from a button.
Set showSwipeHandle to show a grab handle, and showCloseButton on the content for a corner close button, with closeLabel for a text button. snapPoints accepts fractions of the screen such as [0.4, 1]; control the current one with snapPoint and onSnapPointChange.
With modal={false} there is no scrim. For the scaled background, render DrawerIndentBackground behind a DrawerIndent inside DrawerProvider.
Examples
Default


import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"
import { Text } from "@/components/ui/text"
function GoalContent() {
return (
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<View className="items-center gap-1 p-4">
<Text className="text-5xl font-semibold tracking-tight">350</Text>
<Text className="text-xs text-label-secondary uppercase">
Calories/day
</Text>
</View>
<DrawerFooter>
<Button>
<Text>Submit</Text>
</Button>
<DrawerClose render={<Button variant="secondary" />}>
<Text>Cancel</Text>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
)
}
export function DrawerExample() {
return (
<View className="items-start">
<Drawer showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
<Text>Open drawer</Text>
</DrawerTrigger>
<GoalContent />
</Drawer>
</View>
)
}import EsuissUI
import SwiftUI
struct DrawerGoal: View {
@State private var open: Bool
init(open: Bool = false) {
_open = State(initialValue: open)
}
var body: some View {
Button("Open drawer") { open = true }
.buttonStyle(.variant(.outline))
.drawer(isPresented: $open) {
DrawerGoalContent()
}
}
}
struct DrawerGoalContent: View {
var body: some View {
DrawerContent {
DrawerHeader {
DrawerTitle("Move goal")
DrawerDescription("Set your daily activity goal.")
}
VStack(spacing: 4) {
Text(verbatim: "350")
.font(.system(size: 48, weight: .semibold))
.tracking(-1.2)
.foregroundStyle(Color.label)
Text("Calories/day")
.textCase(.uppercase)
.textStyle(.xs)
.foregroundStyle(Color.labelSecondary)
}
.frame(maxWidth: .infinity)
.padding(16)
DrawerFooter {
Button {} label: {
Text("Submit").frame(maxWidth: .infinity)
}
.buttonStyle(.variant())
DrawerClose {
Text("Cancel").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.secondary))
}
}
}
}import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Drawer
@Composable
fun DrawerExample() {
DrawerGoal(defaultOpen = false, floating = false)
}
@Composable
fun DrawerGoal(defaultOpen: Boolean, floating: Boolean, label: String = "Open drawer") {
var open by rememberSaveable { mutableStateOf(defaultOpen) }
Column {
Button(label, onClick = { open = true }, variant = ButtonVariant.Outline)
Drawer(open = open, onOpenChange = { open = it }, floating = floating, showSwipeHandle = true) {
DrawerGoalContent(onClose = { open = false })
}
}
}Snap points
A drawer that rests at 40% and full height.


import { ScrollView, View } from "react-native"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"
import { Text } from "@/components/ui/text"
const ITEMS = Array.from({ length: 20 }, (_, index) => `Item ${index + 1}`)
export function DrawerSnapPointsExample() {
return (
<View className="items-start">
<Drawer snapPoints={[0.4, 1]} showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
<Text>Open with snap points</Text>
</DrawerTrigger>
<DrawerContent showCloseButton>
<DrawerHeader>
<DrawerTitle>Snap points</DrawerTitle>
<DrawerDescription>
Drag up to expand, drag down to collapse or dismiss.
</DrawerDescription>
</DrawerHeader>
<ScrollView contentContainerClassName="gap-3 p-4">
{ITEMS.map((item) => (
<View
key={item}
className="h-12 justify-center rounded-xl bg-surface-secondary px-4"
>
<Text>{item}</Text>
</View>
))}
</ScrollView>
</DrawerContent>
</Drawer>
</View>
)
}import EsuissUI
import SwiftUI
struct DrawerSnapPoints: View {
@State private var open = false
var body: some View {
Button("Open with snap points") { open = true }
.buttonStyle(.variant(.outline))
.drawer(isPresented: $open, detents: [.fraction(0.4), .large]) {
DrawerContent(showCloseButton: true) {
DrawerHeader {
DrawerTitle("Snap points")
DrawerDescription("Drag up to expand, drag down to collapse or dismiss.")
}
ScrollView {
VStack(spacing: 12) {
ForEach(1...20, id: \.self) { index in
Text(verbatim: "Item \(index)")
.textStyle(.base)
.foregroundStyle(Color.label)
.padding(.horizontal, 16)
.frame(maxWidth: .infinity, minHeight: 48, alignment: .leading)
.background(Color.surfaceSecondary, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
}
}
.padding(16)
}
}
}
}
}import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.unit.dp
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Drawer
import com.esuiss.ui.components.DrawerDescription
import com.esuiss.ui.components.DrawerHeader
import com.esuiss.ui.components.DrawerTitle
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
val DrawerItems = List(20) { "Item ${it + 1}" }
@Composable
fun DrawerSnapPoints() {
var open by rememberSaveable { mutableStateOf(false) }
Column {
Button("Open with snap points", onClick = { open = true }, variant = ButtonVariant.Outline)
Drawer(
open = open,
onOpenChange = { open = it },
snapPoints = listOf(0.4f, 1f),
showSwipeHandle = true,
showCloseButton = true,
) {
DrawerHeader {
DrawerTitle("Snap points")
DrawerDescription("Drag up to expand, drag down to collapse or dismiss.")
}
Column(
modifier = Modifier
.weight(1f, fill = false)
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
DrawerItems.forEach { item ->
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.clip(RoundedCornerShape(Radius.xl))
.background(EsuissTheme.colors.surfaceSecondary)
.padding(horizontal = 16.dp),
contentAlignment = Alignment.CenterStart,
) {
Text(item)
}
}
}
}
}
}Floating
A floating drawer inset from the edges.


import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"
import { Text } from "@/components/ui/text"
function GoalContent() {
return (
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<View className="items-center gap-1 p-4">
<Text className="text-5xl font-semibold tracking-tight">350</Text>
<Text className="text-xs text-label-secondary uppercase">
Calories/day
</Text>
</View>
<DrawerFooter>
<Button>
<Text>Submit</Text>
</Button>
<DrawerClose render={<Button variant="secondary" />}>
<Text>Cancel</Text>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
)
}
export function DrawerFloatingExample() {
return (
<View className="items-start">
<Drawer floating showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
<Text>Open floating drawer</Text>
</DrawerTrigger>
<GoalContent />
</Drawer>
</View>
)
}import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Drawer
@Composable
fun DrawerFloatingExample() {
DrawerGoal(defaultOpen = false, floating = true, label = "Open floating drawer")
}
@Composable
fun DrawerGoal(defaultOpen: Boolean, floating: Boolean, label: String = "Open drawer") {
var open by rememberSaveable { mutableStateOf(defaultOpen) }
Column {
Button(label, onClick = { open = true }, variant = ButtonVariant.Outline)
Drawer(open = open, onOpenChange = { open = it }, floating = floating, showSwipeHandle = true) {
DrawerGoalContent(onClose = { open = false })
}
}
}Sides
Drawers from the top, bottom, left and right.


import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"
import { Text } from "@/components/ui/text"
export function DrawerSidesExample() {
return (
<View className="items-start gap-3">
{(["up", "right", "down", "left"] as const).map((direction) => (
<Drawer key={direction} swipeDirection={direction} showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
<Text>Swipe {direction}</Text>
</DrawerTrigger>
<DrawerContent showCloseButton closeLabel="Done">
<DrawerHeader>
<DrawerTitle>Swipe {direction} to close</DrawerTitle>
<DrawerDescription>
This drawer is dismissed by swiping {direction}.
</DrawerDescription>
</DrawerHeader>
<View className="p-4">
<Text className="text-sm text-label-secondary">
Android back and the VoiceOver escape gesture also close it.
</Text>
</View>
</DrawerContent>
</Drawer>
))}
</View>
)
}API Reference
DrawerProvider
Tracks open drawers for the indent effect.
DrawerPrimitive.ProviderPropsDrawerIndentBackground
Background shown behind the scaled screen.
React.ComponentProps<typeof DrawerPrimitive.IndentBackground>DrawerIndent
Screen wrapper that scales back while a bottom drawer is open.
React.ComponentProps<typeof DrawerPrimitive.Indent>Drawer
Root that holds the open state, direction and snap points.
DrawerPrimitive.RootProps- About
- Insets the panel from the edges.
- Type
boolean- Default
- false
- About
- Shows a grab handle.
- Type
boolean- Default
- false
DrawerOverlay
Dimmed scrim behind a modal drawer.
React.ComponentProps<typeof DrawerPrimitive.Backdrop>DrawerSwipeHandle
Grab handle on the dragging edge.
ViewPropsDrawerContent
Sliding panel with optional handle and close button.
React.ComponentProps<typeof DrawerPrimitive.Popup>- About
- Name of the portal host to render into.
- Type
string- Default
- —
- About
- Shows a corner close button.
- Type
boolean- Default
- false
- About
- Text for the close button instead of the icon.
- Type
string- Default
- —
DrawerHeader
Area for the title and description.
ViewPropsDrawerFooter
Area for actions at the end of the panel.
ViewPropsDrawerTitle
Drawer heading, centered for top and bottom drawers.
React.ComponentProps<typeof DrawerPrimitive.Title>DrawerDescription
Supporting text.
React.ComponentProps<typeof DrawerPrimitive.Description>Accessibility
- The popup has the
dialogrole and is labelled by the title. - The swipe handle and decorative edge fill are hidden from screen readers.
- The icon close button is labelled "Close".
Notes
- Padding follows the safe-area insets, and a docked drawer extends its background past the screen edge so drag overshoot shows no gap.
- Animations respect the system Reduce Motion setting.
- Uses
bg-surface-raised,bg-scrimandbg-label-quaternarytokens; light and dark follow the system appearance.