suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Sheet

A panel that slides in from an edge of the screen over the page.

Sheet is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Sheet is built on the dialog primitive of the chosen base. SheetTrigger opens it, and SheetContent renders the panel in a portal over a dimming backdrop. Inside, SheetHeader holds a SheetTitle and SheetDescription, and SheetFooter holds actions. SheetClose closes the sheet from any element.

The panel floats inset from the screen edge with rounded corners and slides in from its side. Use a sheet for supporting tasks, such as editing details or showing navigation, that should keep the page in context. For short, centered confirmations, use Dialog or AlertDialog.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/sheet

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add sheet

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react @phosphor-icons/react
  1. Add the pieces it builds on: Button, use-scrollbar-gap.

  2. Copy the source into components/ui/sheet.tsx:

  1. Update the import paths to match your project.

Usage

import { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } 
<




<




<


Use defaultOpen for an uncontrolled sheet, or open with onOpenChange to control it. Use the render prop on SheetTrigger and SheetClose to render them as a Button.

Use defaultOpen for an uncontrolled sheet, or open with onOpenChange to control it. Set asChild on SheetTrigger and SheetClose to render a Button in their place.

Use defaultOpen for an uncontrolled sheet, or isOpen with onOpenChange to control it. The Button placed first inside Sheet opens it, and a Button with slot="close" closes it.

SheetFooter sits at the bottom of the panel with mt-auto. By default SheetContent adds an icon close button in the top corner; set closeLabel to show a text button instead, or showCloseButton={false} to remove it.

Examples

Sides

Opens a sheet from each of the four edges.

Left

Renders a sheet open on the left side.

Bottom

Renders a sheet open along the bottom edge.

Without close button

Removes the built-in close button.

With close label

Replaces the icon close button with a text button labelled Done.

API Reference

Anatomy2 parts
<Sheet>
<SheetTrigger />
</Sheet>
01

Sheet

The root that holds the open state.

Also accepts the props of the primitive part it wraps.

02

SheetTrigger

The element that opens the sheet.

Also accepts the props of the primitive part it wraps.

03

SheetClose

An element that closes the sheet when pressed.

Also accepts the props of the primitive part it wraps.

04

SheetContent

The sliding panel, rendered in a portal with a backdrop.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""right"Chooses the edge the sheet slides in from.
showCloseButtonbooleantrueShows the built-in close button in the top corner.
closeLabelstring-Shows this text in the close button instead of an icon.

Also accepts the props of the primitive part it wraps.

05

SheetHeader

A padded stack for the title and description.

Also accepts the props of React.ComponentProps<"div">.

06

SheetFooter

A padded stack for actions, pushed to the bottom of the panel.

Also accepts the props of React.ComponentProps<"div">.

07

SheetTitle

The sheet's title, which names the dialog.

Also accepts the props of the primitive part it wraps.

08

SheetDescription

Supporting text, which describes the dialog.

Also accepts the props of the primitive part it wraps.

Accessibility

  • The panel has the dialog role. SheetTitle gives it an accessible name and SheetDescription an accessible description.
  • Opening the sheet moves focus inside it.
  • Escape closes the sheet, and focus returns to the trigger.
  • The icon close button has the hidden label Close.

Notes

  • Parts carry data-slot attributes such as sheet-content, sheet-overlay and sheet-close; the panel also sets data-side.
  • Left and right sheets are 3/4 of the screen wide, up to max-w-sm from the sm breakpoint. Slide directions flip in right-to-left layouts.
  • The panel uses bg-popover and the backdrop uses bg-black/50.

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 shadcn@latest add @esuiss-rn/sheet

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add sheet
$ pnpm dlx @esuiss/ui@latest add sheet --platform swift
NextSidebarPreviousSeparator

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Sides
    • Left
    • Bottom
    • Without close button
    • With close label
  4. 04API Reference
    • Sheet
    • SheetTrigger
    • SheetClose
    • SheetContent
    • SheetHeader
    • SheetFooter
    • SheetTitle
    • SheetDescription
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Default
    • Sides
    • With close label
  10. 10API Reference
    • Sheet
    • SheetContent
    • SheetHeader
    • SheetFooter
    • SheetTitle
    • SheetDescription
  11. 11Accessibility
  12. 12Notes
from
"@/components/ui/sheet"
Sheet
>
<SheetTrigger render={<Button variant="outline" />}>
Open sheet
</SheetTrigger>
<ProfileSheet />
</Sheet>
Sheet
>
<SheetTrigger asChild>
<Button variant="outline">Open sheet</Button>
</SheetTrigger>
<ProfileSheet />
</Sheet>
Sheet
>
<Button variant="outline">Open sheet</Button>
<ProfileSheet />
</Sheet>
$ pnpm dlx @esuiss/ui@latest add sheet --platform kotlin

The CLI also adds what it builds on: Button, Icon, Text. On React Native it also adds the animated view components/ui/animated-view.tsx.

Manual

  1. 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
  1. Add the pieces it builds on: Button, Icon, Text, and the primitives components/primitives/drawer.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

  1. Copy the source into components/ui/sheet.tsx:
  1. Update the import paths to match your project.
  1. 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
  1. Add the pieces it builds on: Button, Icon, Text, and the primitives components/primitives/drawer.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

  1. Copy the source into components/ui/sheet.tsx:
  1. Update the import paths to match your project.
  1. 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
  1. Add the pieces it builds on: Button, Icon, Text, and the primitives components/primitives/drawer.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

  1. Copy the source into components/ui/sheet.tsx:
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
  1. Copy the source and every file it uses:
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
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")
  1. Copy the theme files every view uses:
  1. Copy the source and every file it uses:
  1. Update the com.esuiss.ui package 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

iOS
Sheet on iOS in light modeSheet on iOS in dark mode
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&apos;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.

iOS
Sheet (sides) on iOS in light modeSheet (sides) on iOS in dark mode
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&apos;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.

iOS
Sheet (with close label) on iOS in light modeSheet (with close label) on iOS in dark mode
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

01

Sheet

Root that holds the open state.

Also accepts the props of DrawerPrimitive.RootProps.

02

SheetContent

The panel, rendered in a portal with an overlay and an optional close button.

PropTypeDefaultDescription
portalHoststring-Name of the portal host to render into.
sideSheetSide"right"Edge the sheet slides in from: top, right, bottom or left. Defaults to right.
showCloseButtonbooleantrueShows the close button in the top corner. Defaults to true.
closeLabelstring-Text for the close button. When omitted, an X icon is shown.

Also accepts the props of React.ComponentProps<typeof DrawerPrimitive.Popup>.

03

SheetHeader

Column for the title and description.

Also accepts the props of ViewProps.

04

SheetFooter

Column of actions pushed to the bottom of the panel.

Also accepts the props of ViewProps.

05

SheetTitle

Heading that labels the sheet.

Also accepts the props of React.ComponentProps<typeof DrawerPrimitive.Title>.

06

SheetDescription

Secondary text under the title.

Also accepts the props of React.ComponentProps<typeof DrawerPrimitive.Description>.

Accessibility

  • The panel has the dialog role and is labeled by SheetTitle when 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-popover for the panel and bg-black/50 for the overlay; colors follow the system appearance.
  • On iOS the sheet renders inside a FullWindowOverlay from react-native-screens, so it appears above native modals.
  • Side sheets are three quarters of the screen width, capped at max-w-sm on larger screens.
iOS
Sheet (open by default) on iOS in light modeSheet (open by default) on iOS in dark mode
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&apos;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 })
    }
}
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"

type Side = "top" | "right" | "bottom" | "left"

function ProfileSheet({
  side = "right",
  showCloseButton = true,
  closeLabel,
}: {
  side?: Side
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <SheetContent
      side={side}
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
    >
      <SheetHeader>
        <SheetTitle>Edit profile</SheetTitle>
        <SheetDescription>
          Update your details and save when you are done.
        </SheetDescription>
      </SheetHeader>
      <div className="grid gap-4 px-6">
        <div className="grid gap-2">
          <Label htmlFor="sheet-name">Name</Label>
          <Input id="sheet-name" defaultValue="Jordan Lee" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="sheet-username">Username</Label>
          <Input id="sheet-username" defaultValue="@jordanlee" />
        </div>
      </div>
      <SheetFooter>
        <Button type="submit">Save changes</Button>
        <SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
      </SheetFooter>
    </SheetContent>
  )
}

export default function SheetDemo() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <ProfileSheet />
    </Sheet>
  )
}
components/ui/sheet.tsx
"use client"

import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
import { XIcon } from "@phosphor-icons/react"
import type * as React from "react"
import { Button } from "@/components/ui/button"
import { useScrollbarGap } from "@/hooks/use-scrollbar-gap"
import { cn } from "@/lib/utils"

function Sheet({ ...props }: SheetPrimitive.Root.Props) {
  return <SheetPrimitive.Root data-slot="sheet" {...props} />
}

function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
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"

type Side = "top" | "right" | "bottom" | "left"

function ProfileSheet({
  side = "right",
  showCloseButton = true,
  closeLabel,
}: {
  side?: Side
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <SheetContent
      side={side}
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
    >
      <SheetHeader>
        <SheetTitle>Edit profile</SheetTitle>
        <SheetDescription>
          Update your details and save when you are done.
        </SheetDescription>
      </SheetHeader>
      <div className="grid gap-4 px-6">
        <div className="grid gap-2">
          <Label htmlFor="sheet-name">Name</Label>
          <Input id="sheet-name" defaultValue="Jordan Lee" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="sheet-username">Username</Label>
          <Input id="sheet-username" defaultValue="@jordanlee" />
        </div>
      </div>
      <SheetFooter>
        <Button type="submit">Save changes</Button>
        <SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
      </SheetFooter>
    </SheetContent>
  )
}

export default function SheetSides() {
  return (
    <div className="grid grid-cols-2 gap-3">
      {(["top", "right", "bottom", "left"] as const).map((side) => (
        <Sheet key={side}>
          <SheetTrigger render={<Button variant="outline" />}>
            {side.charAt(0).toUpperCase() + side.slice(1)}
          </SheetTrigger>
          <ProfileSheet side={side} />
        </Sheet>
      ))}
    </div>
  )
}
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"

type Side = "top" | "right" | "bottom" | "left"

function ProfileSheet({
  side = "right",
  showCloseButton = true,
  closeLabel,
}: {
  side?: Side
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <SheetContent
      side={side}
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
    >
      <SheetHeader>
        <SheetTitle>Edit profile</SheetTitle>
        <SheetDescription>
          Update your details and save when you are done.
        </SheetDescription>
      </SheetHeader>
      <div className="grid gap-4 px-6">
        <div className="grid gap-2">
          <Label htmlFor="sheet-name">Name</Label>
          <Input id="sheet-name" defaultValue="Jordan Lee" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="sheet-username">Username</Label>
          <Input id="sheet-username" defaultValue="@jordanlee" />
        </div>
      </div>
      <SheetFooter>
        <Button type="submit">Save changes</Button>
        <SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
      </SheetFooter>
    </SheetContent>
  )
}

export default function SheetOpenLeft() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <ProfileSheet side="left" />
    </Sheet>
  )
}
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"

type Side = "top" | "right" | "bottom" | "left"

function ProfileSheet({
  side = "right",
  showCloseButton = true,
  closeLabel,
}: {
  side?: Side
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <SheetContent
      side={side}
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
    >
      <SheetHeader>
        <SheetTitle>Edit profile</SheetTitle>
        <SheetDescription>
          Update your details and save when you are done.
        </SheetDescription>
      </SheetHeader>
      <div className="grid gap-4 px-6">
        <div className="grid gap-2">
          <Label htmlFor="sheet-name">Name</Label>
          <Input id="sheet-name" defaultValue="Jordan Lee" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="sheet-username">Username</Label>
          <Input id="sheet-username" defaultValue="@jordanlee" />
        </div>
      </div>
      <SheetFooter>
        <Button type="submit">Save changes</Button>
        <SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
      </SheetFooter>
    </SheetContent>
  )
}

export default function SheetOpenBottom() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <ProfileSheet side="bottom" />
    </Sheet>
  )
}
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"

type Side = "top" | "right" | "bottom" | "left"

function ProfileSheet({
  side = "right",
  showCloseButton = true,
  closeLabel,
}: {
  side?: Side
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <SheetContent
      side={side}
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
    >
      <SheetHeader>
        <SheetTitle>Edit profile</SheetTitle>
        <SheetDescription>
          Update your details and save when you are done.
        </SheetDescription>
      </SheetHeader>
      <div className="grid gap-4 px-6">
        <div className="grid gap-2">
          <Label htmlFor="sheet-name">Name</Label>
          <Input id="sheet-name" defaultValue="Jordan Lee" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="sheet-username">Username</Label>
          <Input id="sheet-username" defaultValue="@jordanlee" />
        </div>
      </div>
      <SheetFooter>
        <Button type="submit">Save changes</Button>
        <SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
      </SheetFooter>
    </SheetContent>
  )
}

export default function SheetWithoutCloseButton() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <ProfileSheet showCloseButton={false} />
    </Sheet>
  )
}
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"

type Side = "top" | "right" | "bottom" | "left"

function ProfileSheet({
  side = "right",
  showCloseButton = true,
  closeLabel,
}: {
  side?: Side
  showCloseButton?: boolean
  closeLabel?: string
}) {
  return (
    <SheetContent
      side={side}
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
    >
      <SheetHeader>
        <SheetTitle>Edit profile</SheetTitle>
        <SheetDescription>
          Update your details and save when you are done.
        </SheetDescription>
      </SheetHeader>
      <div className="grid gap-4 px-6">
        <div className="grid gap-2">
          <Label htmlFor="sheet-name">Name</Label>
          <Input id="sheet-name" defaultValue="Jordan Lee" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="sheet-username">Username</Label>
          <Input id="sheet-username" defaultValue="@jordanlee" />
        </div>
      </div>
      <SheetFooter>
        <Button type="submit">Save changes</Button>
        <SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
      </SheetFooter>
    </SheetContent>
  )
}

export default function SheetWithCloseLabel() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <ProfileSheet closeLabel="Done" />
    </Sheet>
  )
}
uniwind/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
uniwind/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,
swift/Theme/Theme.swift
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
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
swift/Components/Sheet.swift
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)
      }
swift/Components/Button.swift
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: 44
swift/Components/ButtonGroup.swift
import 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
}
swift/Components/Dialog.swift
import 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 {
swift/Components/Overlay.swift
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()
  }
}
swift/Components/TextVariant.swift
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
    }
  }
kotlin/theme/Theme.kt
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.DpOffset
kotlin/theme/Tokens.kt
package 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 background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
kotlin/components/Sheet.kt
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.Box
kotlin/components/Button.kt
package 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.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package 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.Immutable
kotlin/components/Content.kt
package 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 }
kotlin/components/Icon.kt
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,
kotlin/components/Overlay.kt
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
nativewind/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
nativewind/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,
stylesheet/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,
stylesheet/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {