suiss/uim · React Native Component
Sheet
A panel that slides in from any edge of the screen, for secondary content and settings.
Sheet 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 { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { Text } from "@/components/ui/text"
type Side = "top" | "right" | "bottom" | "left"
function ProfileContent({ side }: { side?: Side }) {
return (
<SheetContent side={side}>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Tap save when you're done.
</SheetDescription>
</SheetHeader>
<View className="gap-4 px-6">
<View className="gap-2">
<Label nativeID="sheet-name">Name</Label>
<Input
accessibilityLabelledBy="sheet-name"
defaultValue="Pedro Duarte"
/>
</View>
<View className="gap-2">
<Label nativeID="sheet-username">Username</Label>
<Input
accessibilityLabelledBy="sheet-username"
defaultValue="@peduarte"
autoCapitalize="none"
/>
</View>
</View>
<SheetFooter>
<Button>
<Text>Save changes</Text>
</Button>
<SheetClose render={<Button variant="secondary" />}>
<Text>Close</Text>
</SheetClose>
</SheetFooter>
</SheetContent>
)
}
export function SheetOpenByDefaultExample() {
return (
<View className="items-start">
<Sheet defaultOpen>
<SheetTrigger render={<Button variant="outline" />}>
<Text>Open</Text>
</SheetTrigger>
<ProfileContent />
</Sheet>
</View>
)
}import EsuissUI
import SwiftUI
struct SheetOpenByDefaultExample: View {
var body: some View {
ProfileSheet(title: "Open", side: .trailing, open: true)
}
}
struct ProfileSheet: View {
let title: LocalizedStringKey
let side: Edge
@State private var open: Bool
init(title: LocalizedStringKey, side: Edge, open: Bool = false) {
self.title = title
self.side = side
_open = State(initialValue: open)
}
var body: some View {
Button(title) { open = true }
.buttonStyle(.variant(.outline))
.sheet(isPresented: $open, side: side) {
ProfileContent()
}
}
}
struct ProfileContent: View {
@State private var name = "Pedro Duarte"
@State private var username = "@peduarte"
var body: some View {
SheetContent {
SheetHeader {
SheetTitle("Edit profile")
SheetDescription("Make changes to your profile here. Tap save when you're done.")
}
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
FieldLabel("Name")
.accessibilityHidden(true)
TextField("Name", text: $name)
.textFieldStyle(.input)
}
VStack(alignment: .leading, spacing: 8) {
FieldLabel("Username")
.accessibilityHidden(true)
TextField("Username", text: $username)
.textFieldStyle(.input)
.textInputAutocapitalization(.never)
}
}
.padding(.horizontal, 24)
SheetFooter {
Button {} label: {
Text("Save changes").frame(maxWidth: .infinity)
}
.buttonStyle(.variant())
SheetClose {
Text("Close").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.secondary))
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
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.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Label
import com.esuiss.ui.components.Sheet
import com.esuiss.ui.components.SheetDescription
import com.esuiss.ui.components.SheetFooter
import com.esuiss.ui.components.SheetHeader
import com.esuiss.ui.components.SheetSide
import com.esuiss.ui.components.SheetTitle
@Composable
fun SheetOpenByDefaultExample() {
SheetDefault(defaultOpen = true)
}
@Composable
fun ProfileSheet(open: Boolean, onOpenChange: (Boolean) -> Unit, side: SheetSide = SheetSide.Right) {
var name by rememberSaveable { mutableStateOf("Pedro Duarte") }
var username by rememberSaveable { mutableStateOf("@peduarte") }
Sheet(open = open, onOpenChange = onOpenChange, side = side) {
SheetHeader {
SheetTitle("Edit profile")
SheetDescription("Make changes to your profile here. Tap save when you're done.")
}
Column(Modifier.padding(horizontal = 24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Label("Name")
Input(name, { name = it }, Modifier.semantics { contentDescription = "Name" })
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Label("Username")
Input(
username,
{ username = it },
Modifier.semantics { contentDescription = "Username" },
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false),
)
}
}
SheetFooter {
Button("Save changes", onClick = {}, modifier = Modifier.fillMaxWidth())
Button("Close", onClick = { onOpenChange(false) }, modifier = Modifier.fillMaxWidth(), variant = ButtonVariant.Secondary)
}
}
}
@Composable
fun SheetDefault(defaultOpen: Boolean) {
var open by rememberSaveable { mutableStateOf(defaultOpen) }
Column {
Button("Open", onClick = { open = true }, variant = ButtonVariant.Outline)
ProfileSheet(open, { open = it })
}
}Sheet is built on the drawer primitive. SheetContent renders in a portal, above a dimmed overlay, as a rounded floating panel inset from the screen edges and the safe area. It enters and exits with a Reanimated fade-and-slide from its side and can be dismissed by swiping toward that edge.
Use right or left for navigation and detail panels that span the screen height, and top or bottom for short panels that span the width. For snap points and a drag handle, use Drawer instead.
Installation
$ pnpm dlx @esuiss/uim@latest add sheet$ pnpm dlx @esuiss/uim@latest add sheet --platform swift$ pnpm dlx @esuiss/uim@latest add sheet --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/sheet.tsx:
import { cn } from "cn"
import { XIcon } from "phosphor-react-native"
import * as React from "react"
import { Platform, View, type ViewProps, type ViewStyle } from "react-native"
import {
FadeIn,
FadeInDown,
FadeInLeft,
FadeInRight,
FadeInUp,
FadeOut,
FadeOutDown,
FadeOutLeft,
FadeOutRight,
FadeOutUp,
ReduceMotion,- 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/sheet.tsx:
import { cn } from "cn"
import { XIcon } from "phosphor-react-native"
import * as React from "react"
import { Platform, View, type ViewProps, type ViewStyle } from "react-native"
import {
FadeIn,
FadeInDown,
FadeInLeft,
FadeInRight,
FadeInUp,
FadeOut,
FadeOutDown,
FadeOutLeft,
FadeOutRight,
FadeOutUp,
ReduceMotion,- 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/sheet.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,
FadeInDown,
FadeInLeft,
FadeInRight,
FadeInUp,- 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 sheet<Content: View>(
isPresented: Binding<Bool>,
side: Edge,
@ViewBuilder content: @escaping () -> Content
) -> some View {
modifier(
CoverPresenter(
isPresented: isPresented,
enter: .easeOut(duration: 0.4),
exit: .easeIn(duration: 0.3)
) { visible in
SideSheetStage(side: side, visible: visible, content: content)
}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.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Boxpackage 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 { Sheet, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from "@/components/ui/sheet"<View className="items-start">
<Sheet defaultOpen>
<SheetTrigger render={<Button variant="outline" />}>
<Text>Open</Text>
</SheetTrigger>
<ProfileContent />
</Sheet>
</View>Wrap a SheetTrigger and a SheetContent in Sheet. Inside the content, compose SheetHeader with SheetTitle and SheetDescription, your body, and a SheetFooter, which is pushed to the bottom. Use SheetTrigger and SheetClose with render to turn a Button into a trigger or close control.
Sheet accepts open, defaultOpen and onOpenChange for controlled or uncontrolled use, plus modal, disablePointerDismissal, onOpenChangeComplete and actionsRef. The swipe direction follows the side of its content.
A close button is shown in the top corner by default. Set closeLabel to show a text button instead of the X icon, or showCloseButton={false} to remove it. SheetHeader adds end padding so the title does not run under the button.
Examples
Default


import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { Text } from "@/components/ui/text"
type Side = "top" | "right" | "bottom" | "left"
function ProfileContent({ side }: { side?: Side }) {
return (
<SheetContent side={side}>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Tap save when you're done.
</SheetDescription>
</SheetHeader>
<View className="gap-4 px-6">
<View className="gap-2">
<Label nativeID="sheet-name">Name</Label>
<Input
accessibilityLabelledBy="sheet-name"
defaultValue="Pedro Duarte"
/>
</View>
<View className="gap-2">
<Label nativeID="sheet-username">Username</Label>
<Input
accessibilityLabelledBy="sheet-username"
defaultValue="@peduarte"
autoCapitalize="none"
/>
</View>
</View>
<SheetFooter>
<Button>
<Text>Save changes</Text>
</Button>
<SheetClose render={<Button variant="secondary" />}>
<Text>Close</Text>
</SheetClose>
</SheetFooter>
</SheetContent>
)
}
export function SheetExample() {
return (
<View className="items-start">
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>
<Text>Open</Text>
</SheetTrigger>
<ProfileContent />
</Sheet>
</View>
)
}import EsuissUI
import SwiftUI
struct SheetExample: View {
var body: some View {
ProfileSheet(title: "Open", side: .trailing)
}
}
struct ProfileSheet: View {
let title: LocalizedStringKey
let side: Edge
@State private var open: Bool
init(title: LocalizedStringKey, side: Edge, open: Bool = false) {
self.title = title
self.side = side
_open = State(initialValue: open)
}
var body: some View {
Button(title) { open = true }
.buttonStyle(.variant(.outline))
.sheet(isPresented: $open, side: side) {
ProfileContent()
}
}
}
struct ProfileContent: View {
@State private var name = "Pedro Duarte"
@State private var username = "@peduarte"
var body: some View {
SheetContent {
SheetHeader {
SheetTitle("Edit profile")
SheetDescription("Make changes to your profile here. Tap save when you're done.")
}
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
FieldLabel("Name")
.accessibilityHidden(true)
TextField("Name", text: $name)
.textFieldStyle(.input)
}
VStack(alignment: .leading, spacing: 8) {
FieldLabel("Username")
.accessibilityHidden(true)
TextField("Username", text: $username)
.textFieldStyle(.input)
.textInputAutocapitalization(.never)
}
}
.padding(.horizontal, 24)
SheetFooter {
Button {} label: {
Text("Save changes").frame(maxWidth: .infinity)
}
.buttonStyle(.variant())
SheetClose {
Text("Close").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.secondary))
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
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.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Label
import com.esuiss.ui.components.Sheet
import com.esuiss.ui.components.SheetDescription
import com.esuiss.ui.components.SheetFooter
import com.esuiss.ui.components.SheetHeader
import com.esuiss.ui.components.SheetSide
import com.esuiss.ui.components.SheetTitle
@Composable
fun SheetExample() {
SheetDefault(defaultOpen = false)
}
@Composable
fun ProfileSheet(open: Boolean, onOpenChange: (Boolean) -> Unit, side: SheetSide = SheetSide.Right) {
var name by rememberSaveable { mutableStateOf("Pedro Duarte") }
var username by rememberSaveable { mutableStateOf("@peduarte") }
Sheet(open = open, onOpenChange = onOpenChange, side = side) {
SheetHeader {
SheetTitle("Edit profile")
SheetDescription("Make changes to your profile here. Tap save when you're done.")
}
Column(Modifier.padding(horizontal = 24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Label("Name")
Input(name, { name = it }, Modifier.semantics { contentDescription = "Name" })
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Label("Username")
Input(
username,
{ username = it },
Modifier.semantics { contentDescription = "Username" },
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false),
)
}
}
SheetFooter {
Button("Save changes", onClick = {}, modifier = Modifier.fillMaxWidth())
Button("Close", onClick = { onOpenChange(false) }, modifier = Modifier.fillMaxWidth(), variant = ButtonVariant.Secondary)
}
}
}
@Composable
fun SheetDefault(defaultOpen: Boolean) {
var open by rememberSaveable { mutableStateOf(defaultOpen) }
Column {
Button("Open", onClick = { open = true }, variant = ButtonVariant.Outline)
ProfileSheet(open, { open = it })
}
}Sides
One sheet for each side: top, right, bottom and left.


import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { Text } from "@/components/ui/text"
type Side = "top" | "right" | "bottom" | "left"
function ProfileContent({ side }: { side?: Side }) {
return (
<SheetContent side={side}>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Tap save when you're done.
</SheetDescription>
</SheetHeader>
<View className="gap-4 px-6">
<View className="gap-2">
<Label nativeID="sheet-name">Name</Label>
<Input
accessibilityLabelledBy="sheet-name"
defaultValue="Pedro Duarte"
/>
</View>
<View className="gap-2">
<Label nativeID="sheet-username">Username</Label>
<Input
accessibilityLabelledBy="sheet-username"
defaultValue="@peduarte"
autoCapitalize="none"
/>
</View>
</View>
<SheetFooter>
<Button>
<Text>Save changes</Text>
</Button>
<SheetClose render={<Button variant="secondary" />}>
<Text>Close</Text>
</SheetClose>
</SheetFooter>
</SheetContent>
)
}
export function SheetSidesExample() {
return (
<View className="items-start gap-3">
{(["top", "right", "bottom", "left"] as const).map((side) => (
<Sheet key={side}>
<SheetTrigger render={<Button variant="outline" />}>
<Text className="capitalize">{side}</Text>
</SheetTrigger>
<ProfileContent side={side} />
</Sheet>
))}
</View>
)
}import EsuissUI
import SwiftUI
struct SheetSidesExample: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
ProfileSheet(title: "Top", side: .top)
ProfileSheet(title: "Right", side: .trailing)
ProfileSheet(title: "Bottom", side: .bottom)
ProfileSheet(title: "Left", side: .leading)
}
}
}
struct ProfileSheet: View {
let title: LocalizedStringKey
let side: Edge
@State private var open: Bool
init(title: LocalizedStringKey, side: Edge, open: Bool = false) {
self.title = title
self.side = side
_open = State(initialValue: open)
}
var body: some View {
Button(title) { open = true }
.buttonStyle(.variant(.outline))
.sheet(isPresented: $open, side: side) {
ProfileContent()
}
}
}
struct ProfileContent: View {
@State private var name = "Pedro Duarte"
@State private var username = "@peduarte"
var body: some View {
SheetContent {
SheetHeader {
SheetTitle("Edit profile")
SheetDescription("Make changes to your profile here. Tap save when you're done.")
}
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
FieldLabel("Name")
.accessibilityHidden(true)
TextField("Name", text: $name)
.textFieldStyle(.input)
}
VStack(alignment: .leading, spacing: 8) {
FieldLabel("Username")
.accessibilityHidden(true)
TextField("Username", text: $username)
.textFieldStyle(.input)
.textInputAutocapitalization(.never)
}
}
.padding(.horizontal, 24)
SheetFooter {
Button {} label: {
Text("Save changes").frame(maxWidth: .infinity)
}
.buttonStyle(.variant())
SheetClose {
Text("Close").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.secondary))
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
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.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Label
import com.esuiss.ui.components.Sheet
import com.esuiss.ui.components.SheetDescription
import com.esuiss.ui.components.SheetFooter
import com.esuiss.ui.components.SheetHeader
import com.esuiss.ui.components.SheetSide
import com.esuiss.ui.components.SheetTitle
@Composable
fun ProfileSheet(open: Boolean, onOpenChange: (Boolean) -> Unit, side: SheetSide = SheetSide.Right) {
var name by rememberSaveable { mutableStateOf("Pedro Duarte") }
var username by rememberSaveable { mutableStateOf("@peduarte") }
Sheet(open = open, onOpenChange = onOpenChange, side = side) {
SheetHeader {
SheetTitle("Edit profile")
SheetDescription("Make changes to your profile here. Tap save when you're done.")
}
Column(Modifier.padding(horizontal = 24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Label("Name")
Input(name, { name = it }, Modifier.semantics { contentDescription = "Name" })
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Label("Username")
Input(
username,
{ username = it },
Modifier.semantics { contentDescription = "Username" },
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false),
)
}
}
SheetFooter {
Button("Save changes", onClick = {}, modifier = Modifier.fillMaxWidth())
Button("Close", onClick = { onOpenChange(false) }, modifier = Modifier.fillMaxWidth(), variant = ButtonVariant.Secondary)
}
}
}
@Composable
fun SheetSides() {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
SheetSide.entries.forEach { side ->
var open by rememberSaveable(side.name) { mutableStateOf(false) }
Button(side.name, onClick = { open = true }, variant = ButtonVariant.Outline)
ProfileSheet(open, { open = it }, side)
}
}
}With close label
A bottom sheet whose close button reads "Done" instead of showing an icon.


import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { Text } from "@/components/ui/text"
export function SheetWithCloseLabelExample() {
return (
<View className="items-start">
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>
<Text>Open with label</Text>
</SheetTrigger>
<SheetContent side="bottom" closeLabel="Done">
<SheetHeader>
<SheetTitle>Notifications</SheetTitle>
<SheetDescription>
Choose what you want to be notified about.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
</View>
)
}import EsuissUI
import SwiftUI
struct CloseLabelSheet: View {
@State private var open = false
var body: some View {
Button("Open with label") { open = true }
.buttonStyle(.variant(.outline))
.sheet(isPresented: $open, side: .bottom) {
SheetContent(closeLabel: "Done") {
SheetHeader {
SheetTitle("Notifications")
SheetDescription("Choose what you want to be notified about.")
}
}
}
}
}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.Sheet
import com.esuiss.ui.components.SheetDescription
import com.esuiss.ui.components.SheetHeader
import com.esuiss.ui.components.SheetSide
import com.esuiss.ui.components.SheetTitle
@Composable
fun SheetWithCloseLabel() {
var open by rememberSaveable { mutableStateOf(false) }
Column {
Button("Open with label", onClick = { open = true }, variant = ButtonVariant.Outline)
Sheet(open = open, onOpenChange = { open = it }, side = SheetSide.Bottom, closeLabel = "Done") {
SheetHeader {
SheetTitle("Notifications")
SheetDescription("Choose what you want to be notified about.")
}
}
}
}API Reference
Sheet
Root that holds the open state.
DrawerPrimitive.RootPropsSheetContent
The panel, rendered in a portal with an overlay and an optional close button.
React.ComponentProps<typeof DrawerPrimitive.Popup>- About
- Name of the portal host to render into.
- Type
string- Default
- —
- About
- Edge the sheet slides in from:
top,right,bottomorleft. Defaults toright. - Type
SheetSide- Default
- "right"
- About
- Shows the close button in the top corner. Defaults to
true. - Type
boolean- Default
- true
- About
- Text for the close button. When omitted, an X icon is shown.
- Type
string- Default
- —
SheetHeader
Column for the title and description.
ViewPropsSheetFooter
Column of actions pushed to the bottom of the panel.
ViewPropsSheetTitle
Heading that labels the sheet.
React.ComponentProps<typeof DrawerPrimitive.Title>SheetDescription
Secondary text under the title.
React.ComponentProps<typeof DrawerPrimitive.Description>Accessibility
- The panel has the
dialogrole and is labeled bySheetTitlewhen one is present. - It is modal by default, so VoiceOver keeps focus inside the sheet.
- The VoiceOver escape gesture and the Android back button close the sheet.
- The icon-only close button has the accessibility label "Close".
- Animations respect the system reduce motion setting.
Notes
- Uses
bg-surface-raisedfor the panel andbg-scrimfor the overlay; colors follow the system appearance. - On iOS the sheet renders inside a
FullWindowOverlayfromreact-native-screens, so it appears above native modals. - Side sheets are three quarters of the screen width, capped at
max-w-smon larger screens.