suiss/uim · React Native Component
Screen
A full-screen container that pads for safe area insets and sets the status bar style.
Screen 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 { Screen, ScreenContent } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
function Rows({ count = 4 }: { count?: number }) {
return (
<View className="overflow-hidden rounded-2xl bg-surface-secondary">
{Array.from({ length: count }, (_, index) => `Row ${index + 1}`).map(
(label, index) => (
<View
key={label}
className={
index > 0 ? "border-t border-separator px-4 py-3" : "px-4 py-3"
}
>
<Text>{label}</Text>
</View>
)
)}
</View>
)
}
export function ScreenExample() {
return (
<Screen className="-m-4">
<ScreenContent>
<Text variant="h2">Account</Text>
<Text variant="muted">
The screen pads every safe-area edge and sets the status bar style
from the color scheme.
</Text>
<Rows />
</ScreenContent>
</Screen>
)
}import EsuissUI
import SwiftUI
struct ScreenExample: View {
var body: some View {
Screen {
ScreenContent(scroll: false) {
Text("Account").textVariant(.h2)
Text("The screen pads every safe-area edge and sets the status bar style from the color scheme.")
.textVariant(.muted)
ScreenRows()
}
}
}
}
struct ScreenRows: View {
var labels: [String] = (1...4).map { "Row \($0)" }
var background: Color = .surfaceSecondary
var body: some View {
VStack(spacing: 0) {
ForEach(Array(labels.enumerated()), id: \.offset) { index, label in
Text(verbatim: label)
.textVariant(.default)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.overlay(alignment: .top) {
if index > 0 {
Color.separator.frame(height: 1)
}
}
}
}
.background(background)
.clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
}
}import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenContent
import com.esuiss.ui.components.Separator
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
@Composable
fun ScreenRows(labels: List<String>, background: Color) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(Radius.xl2))
.background(background),
) {
labels.forEachIndexed { index, label ->
if (index > 0) Separator()
Text(label, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp))
}
}
}
fun screenRowLabels(count: Int): List<String> = (1..count).map { "Row $it" }
@Composable
fun ScreenDefault() {
Screen {
ScreenContent {
Text("Account", variant = TextVariant.H2)
Text(
"The screen pads every safe-area edge and sets the status bar style from the color scheme.",
variant = TextVariant.Muted,
)
ScreenRows(screenRowLabels(4), EsuissTheme.colors.surfaceSecondary)
}
}
}Screen is a View that fills the screen, paints the surface background and adds padding for the safe area insets on the edges you choose. It also renders a StatusBar with light or dark content to match the color scheme.
ScreenContent lays out the body with padding and gaps. With scrollable, it renders a KeyboardScrollView so content scrolls and stays above the keyboard.
Installation
$ pnpm dlx @esuiss/uim@latest add screen$ pnpm dlx @esuiss/uim@latest add screen --platform swift$ pnpm dlx @esuiss/uim@latest add screen --platform kotlinThe CLI also adds what it builds on: Keyboard Scroll View.
Manual
- Install the dependencies:
npx expo install cn react-native-safe-area-context-
Add the pieces it builds on: Keyboard Scroll View.
-
Copy the source into
components/ui/screen.tsx:
import { cn } from "cn"
import * as React from "react"
import { StatusBar, useColorScheme, View, type ViewProps } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
type ScreenEdge = "top" | "right" | "bottom" | "left"
const ALL_EDGES: readonly ScreenEdge[] = ["top", "right", "bottom", "left"]
const ScreenContext = React.createContext<{
edges: readonly ScreenEdge[]
} | null>(null)
function Screen({
edges = ALL_EDGES,- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn react-native-safe-area-context-
Add the pieces it builds on: Keyboard Scroll View.
-
Copy the source into
components/ui/screen.tsx:
import { cn } from "cn"
import * as React from "react"
import { StatusBar, useColorScheme, View, type ViewProps } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
type ScreenEdge = "top" | "right" | "bottom" | "left"
const ALL_EDGES: readonly ScreenEdge[] = ["top", "right", "bottom", "left"]
const ScreenContext = React.createContext<{
edges: readonly ScreenEdge[]
} | null>(null)
function Screen({
edges = ALL_EDGES,- Update the import paths to match your project.
- Install the dependencies:
npx expo install react-native-safe-area-context-
Add the pieces it builds on: Keyboard Scroll View.
-
Copy the source into
components/ui/screen.tsx:
import * as React from "react"
import { StatusBar, StyleSheet, View, type ViewProps } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
import { useTheme } from "@/components/ui/theme"
type ScreenEdge = "top" | "right" | "bottom" | "left"
const ALL_EDGES: readonly ScreenEdge[] = ["top", "right", "bottom", "left"]
const ScreenContext = React.createContext<{
edges: readonly ScreenEdge[]
} | null>(null)
const styles = StyleSheet.create({
root: { flex: 1 },- Update the import paths to match your project.
-
Target iOS 17 or later.
-
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 EnvironmentValues {
@Entry var screenBackground: Color = .surface
}
public struct Screen<Content: View>: View {
public enum Background: CaseIterable, Sendable {
case surface, surfaceSecondary
var color: Color {
switch self {
case .surface: .surface
case .surfaceSecondary: .surfaceSecondary
}
}import SwiftUI
import UIKit
public struct KeyboardScrollView<Content: View>: View {
let showsIndicators: Bool
let content: Content
public init(showsIndicators: Bool = true, @ViewBuilder content: () -> Content) {
self.showsIndicators = showsIndicators
self.content = content()
}
public var body: some View {
ScrollView(.vertical, showsIndicators: showsIndicators) {
content
.frame(maxWidth: .infinity, alignment: .topLeading)- 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")- 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 android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowInsetsController
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.ColumnScopepackage 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 }- Update the
com.esuiss.uipackage names to match your project.
Usage
import { Screen, ScreenContent } from "@/components/ui/screen"<Screen className="-m-4">
<ScreenContent>
<Text variant="h2">Account</Text>
<Text variant="muted">
The screen pads every safe-area edge and sets the status bar style
from the color scheme.
</Text>
<Rows />
</ScreenContent>
</Screen>Wrap each screen in Screen and put the body in ScreenContent. Use variant="secondary" for grouped screens such as settings.
Pass edges to pad only some sides, for example ["top"] when a tab bar or bottom bar handles the bottom inset. Set scrollable on ScreenContent for content taller than the screen or for forms.
The app must be wrapped in a SafeAreaProvider from react-native-safe-area-context. ScreenContext exposes the active edges to children.
Examples
Secondary
A screen with the secondary background.


import { View } from "react-native"
import { Screen, ScreenContent } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
export function ScreenSecondaryExample() {
return (
<Screen variant="secondary" className="-m-4">
<ScreenContent>
<Text variant="h2">Settings</Text>
<View className="overflow-hidden rounded-2xl bg-surface">
{["General", "Privacy", "Notifications"].map((label, index) => (
<View
key={label}
className={
index > 0 ? "border-t border-separator px-4 py-3" : "px-4 py-3"
}
>
<Text>{label}</Text>
</View>
))}
</View>
</ScreenContent>
</Screen>
)
}import EsuissUI
import SwiftUI
struct ScreenSecondaryExample: View {
var body: some View {
Screen(background: .surfaceSecondary) {
ScreenContent(scroll: false) {
Text("Settings").textVariant(.h2)
ScreenRows(labels: ["General", "Privacy", "Notifications"], background: .surface)
}
}
}
}
struct ScreenRows: View {
var labels: [String] = (1...4).map { "Row \($0)" }
var background: Color = .surfaceSecondary
var body: some View {
VStack(spacing: 0) {
ForEach(Array(labels.enumerated()), id: \.offset) { index, label in
Text(verbatim: label)
.textVariant(.default)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.overlay(alignment: .top) {
if index > 0 {
Color.separator.frame(height: 1)
}
}
}
}
.background(background)
.clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
}
}import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenContent
import com.esuiss.ui.components.ScreenVariant
import com.esuiss.ui.components.Separator
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
@Composable
fun ScreenRows(labels: List<String>, background: Color) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(Radius.xl2))
.background(background),
) {
labels.forEachIndexed { index, label ->
if (index > 0) Separator()
Text(label, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp))
}
}
}
@Composable
fun ScreenSecondary() {
Screen(variant = ScreenVariant.Secondary) {
ScreenContent {
Text("Settings", variant = TextVariant.H2)
ScreenRows(listOf("General", "Privacy", "Notifications"), EsuissTheme.colors.surface)
}
}
}Scrollable
A screen whose content scrolls.


import { View } from "react-native"
import { Screen, ScreenContent } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
function Rows({ count = 4 }: { count?: number }) {
return (
<View className="overflow-hidden rounded-2xl bg-surface-secondary">
{Array.from({ length: count }, (_, index) => `Row ${index + 1}`).map(
(label, index) => (
<View
key={label}
className={
index > 0 ? "border-t border-separator px-4 py-3" : "px-4 py-3"
}
>
<Text>{label}</Text>
</View>
)
)}
</View>
)
}
export function ScreenScrollableExample() {
return (
<Screen className="-m-4">
<ScreenContent scrollable>
<Text variant="h2">Long content</Text>
<Rows count={24} />
</ScreenContent>
</Screen>
)
}import EsuissUI
import SwiftUI
struct ScreenScrollableExample: View {
var body: some View {
Screen {
ScreenContent {
Text("Long content").textVariant(.h2)
ScreenRows(labels: (1...24).map { "Row \($0)" })
}
}
}
}
struct ScreenRows: View {
var labels: [String] = (1...4).map { "Row \($0)" }
var background: Color = .surfaceSecondary
var body: some View {
VStack(spacing: 0) {
ForEach(Array(labels.enumerated()), id: \.offset) { index, label in
Text(verbatim: label)
.textVariant(.default)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.overlay(alignment: .top) {
if index > 0 {
Color.separator.frame(height: 1)
}
}
}
}
.background(background)
.clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
}
}import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenContent
import com.esuiss.ui.components.Separator
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
@Composable
fun ScreenRows(labels: List<String>, background: Color) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(Radius.xl2))
.background(background),
) {
labels.forEachIndexed { index, label ->
if (index > 0) Separator()
Text(label, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp))
}
}
}
fun screenRowLabels(count: Int): List<String> = (1..count).map { "Row $it" }
@Composable
fun ScreenScrollable() {
Screen {
ScreenContent(Modifier.fillMaxSize(), scrollable = true) {
Text("Long content", variant = TextVariant.H2)
ScreenRows(screenRowLabels(24), EsuissTheme.colors.surfaceSecondary)
}
}
}Top edge only
A screen that pads only the top inset.


import { Screen, ScreenContent } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
export function ScreenTopEdgeOnlyExample() {
return (
<Screen edges={["top"]} className="-m-4">
<ScreenContent className="bg-surface-secondary">
<Text variant="h3">Top edge only</Text>
<Text variant="muted">
The bottom, left and right edges are not padded.
</Text>
</ScreenContent>
</Screen>
)
}import EsuissUI
import SwiftUI
struct ScreenTopEdgeOnlyExample: View {
var body: some View {
Screen(edges: .top) {
ScreenContent(scroll: false) {
Text("Top edge only").textVariant(.h3)
Text("The bottom, left and right edges are not padded.")
.textVariant(.muted)
}
.background(Color.surfaceSecondary)
}
}
}import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenContent
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
@Composable
fun ScreenTopEdgeOnly() {
Screen(edges = setOf(ScreenEdge.Top)) {
ScreenContent(Modifier.background(EsuissTheme.colors.surfaceSecondary)) {
Text("Top edge only", variant = TextVariant.H3)
Text("The bottom, left and right edges are not padded.", variant = TextVariant.Muted)
}
}
}With bottom bar
A scrollable form with a bottom bar holding the main action.


import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Screen, ScreenContent } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
export function ScreenWithBottomBarExample() {
return (
<Screen className="-m-4">
<ScreenContent scrollable>
<Text variant="h2">Your email</Text>
<Text variant="muted">We will send a code to this address.</Text>
<Input placeholder="[email protected]" keyboardType="email-address" />
</ScreenContent>
<BottomBar separator>
<BottomBarContent>
<Button size="lg" block>
<Text>Continue</Text>
</Button>
</BottomBarContent>
</BottomBar>
</Screen>
)
}import EsuissUI
import SwiftUI
struct ScreenBottomBarDemo: View {
@State private var email = ""
var body: some View {
Screen {
ScreenContent {
Text("Your email").textVariant(.h2)
Text("We will send a code to this address.").textVariant(.muted)
TextField(String("[email protected]"), text: $email)
.textFieldStyle(.input)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
}
BottomBar(separator: true) {
BottomBarContent {
Button {} label: {
Text("Continue")
}
.buttonStyle(.variant(size: .lg, block: true))
}
}
}
}
}import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
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.text.input.KeyboardType
import com.esuiss.ui.components.BottomBar
import com.esuiss.ui.components.BottomBarContent
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenContent
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
@Composable
fun ScreenWithBottomBar() {
var email by rememberSaveable { mutableStateOf("") }
Screen {
Column(Modifier.fillMaxSize()) {
ScreenContent(Modifier.weight(1f), scrollable = true) {
Text("Your email", variant = TextVariant.H2)
Text("We will send a code to this address.", variant = TextVariant.Muted)
Input(
email,
{ email = it },
placeholder = "[email protected]",
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
)
}
BottomBar(separator = true) {
BottomBarContent {
Button("Continue", onClick = {}, size = ButtonSize.Lg, block = true)
}
}
}
}
}API Reference
Screen
The root view that pads for safe area insets.
ViewProps- About
- Which sides get safe area padding: any of
top,right,bottom,left. Defaults to all four. - Type
readonly ScreenEdge[]- Default
- ALL_EDGES
- About
- Background:
defaultorsecondary. - Type
"default""secondary"- Default
- "default"
ScreenContent
The padded body, optionally scrollable.
ViewProps- About
- Renders a
KeyboardScrollViewinstead of aView. Defaults tofalse. - Type
boolean- Default
- false
Notes
- Uses the
bg-surfaceandbg-surface-secondarytokens. - The status bar uses
light-contentin dark mode anddark-contentin light mode, following the system appearance. - Requires
react-native-safe-area-contextand the Keyboard Scroll View component.