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

Bottom Bar

A bar pinned to the bottom of a screen for primary actions, which rises above the keyboard.

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

iOS
Bottom Bar on iOS in light modeBottom Bar on iOS in dark mode
import { View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"

function Page({ children }: { children: React.ReactNode }) {
  return (
    <View className="-m-4 flex-1">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">Review order</Text>
        <Text variant="muted">2 items, delivery on Thursday.</Text>
      </View>
      {children}
    </View>
  )
}

export function BottomBarExample() {
  return (
    <Page>
      <BottomBar>
        <BottomBarContent>
          <Button size="lg" block>
            <Text>Continue</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Page>
  )
}
import EsuissUI
import SwiftUI

struct BottomBarExample: View {
  var body: some View {
    BottomBarPage {
      BottomBar(separator: false) {
        BottomBarContent {
          Button("Continue") {}.buttonStyle(.variant(size: .lg, block: true))
        }
      }
    }
  }
}

struct BottomBarPage<Bar: View>: View {
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text("Review order").textVariant(.h3)
        Text("2 items, delivery on Thursday.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun BottomBarPage(page: @Composable ColumnScope.() -> Unit, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
                content = page,
            )
            bar()
        }
    }
}

@Composable
fun BottomBarOrder(bar: @Composable () -> Unit) {
    BottomBarPage(
        page = {
            Text("Review order", variant = TextVariant.H3)
            Text("2 items, delivery on Thursday.", variant = TextVariant.Muted)
        },
        bar = bar,
    )
}

@Composable
fun BottomBarDefault() {
    BottomBarOrder {
        BottomBar {
            BottomBarContent {
                Button("Continue", onClick = {}, size = ButtonSize.Lg, block = true)
            }
        }
    }
}

BottomBar is an animated View that adds bottom padding equal to the safe-area inset, or to the keyboard height while the keyboard is open, and animates between them with the keyboard's own timing. BottomBarContent adds the standard padding and spacing for the buttons inside.

Use it for the main call to action of a screen, such as checkout or a form submit button.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/bottom-bar

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

$ pnpm dlx @esuiss/ui@latest add bottom-bar
$ pnpm dlx @esuiss/ui@latest add bottom-bar --platform swift
$ pnpm dlx @esuiss/ui@latest add bottom-bar --platform kotlin

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

Manual

  1. Install the dependencies:
npx expo install cn react-native-reanimated react-native-safe-area-context react-native-worklets
  1. Add the pieces it builds on: Screen, and the primitives components/primitives/keyboard.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:

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) {
  1. Copy the source into components/ui/bottom-bar.tsx:
uniwind/components/ui/bottom-bar.tsx
import { cn } from "cn"
import * as React from "react"
import { View, type ViewProps } from "react-native"
import {
  Easing,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useKeyboard } from "@/components/primitives/keyboard"
import { AnimatedView } from "@/components/ui/animated-view"
import { ScreenContext } from "@/components/ui/screen"

const EASING = Easing.bezier(0.17, 0.59, 0.4, 0.77)
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn react-native-reanimated react-native-safe-area-context react-native-worklets
  1. Add the pieces it builds on: Screen, and the primitives components/primitives/keyboard.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:

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) {
  1. Copy the source into components/ui/bottom-bar.tsx:
nativewind/components/ui/bottom-bar.tsx
import { cn } from "cn"
import * as React from "react"
import { View, type ViewProps } from "react-native"
import {
  Easing,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useKeyboard } from "@/components/primitives/keyboard"
import { AnimatedView } from "@/components/ui/animated-view"
import { ScreenContext } from "@/components/ui/screen"

const EASING = Easing.bezier(0.17, 0.59, 0.4, 0.77)
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install react-native-reanimated react-native-safe-area-context react-native-worklets
  1. Add the pieces it builds on: Screen, and the primitives components/primitives/keyboard.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:

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) {
  1. Copy the source into components/ui/bottom-bar.tsx:
stylesheet/components/ui/bottom-bar.tsx
import * as React from "react"
import { StyleSheet, View, type ViewProps } from "react-native"
import {
  Easing,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useKeyboard } from "@/components/primitives/keyboard"
import { AnimatedView } from "@/components/ui/animated-view"
import { ScreenContext } from "@/components/ui/screen"
import { useTheme } from "@/components/ui/theme"

const EASING = Easing.bezier(0.17, 0.59, 0.4, 0.77)
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Copy the theme files every view uses:

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))
  1. Copy the source and every file it uses:
swift/Components/BottomBar.swift
import SwiftUI

public struct BottomBar<Content: View>: View {
  let separator: Bool
  let content: Content

  @Environment(\.screenBackground) private var background

  public init(separator: Bool = true, @ViewBuilder content: () -> Content) {
    self.separator = separator
    self.content = content()
  }

  public var body: some View {
    VStack(spacing: 0) {
      content
swift/Components/KeyboardScrollView.swift
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)
swift/Components/Screen.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var screenBackground: Color = EsuissColor.background
}

public struct Screen<Content: View>: View {
  public enum Background: CaseIterable, Sendable {
    case background, muted

    var color: Color {
      switch self {
      case .background: EsuissColor.background
      case .muted: EsuissColor.muted
      }
    }
  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")
  1. Copy the theme files every view uses:
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,
  1. Copy the source and every file it uses:
kotlin/components/BottomBar.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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/Screen.kt
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.ColumnScope
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
<Page>
  <BottomBar>
    <BottomBarContent>
      <Button size="lg" block>
        <Text>Continue</Text>
      </Button>
    </BottomBarContent>
  </BottomBar>
</Page>

Place BottomBar as the last child of the screen so it sits below the scrolling content. Stack buttons in BottomBarContent, or add flex-row to place them side by side.

Set separator to draw a hairline above the bar. Inside a Screen that already handles the bottom safe-area edge, the inset is not added twice.

Examples

With separator

A hairline above the bar with separator.

iOS
Bottom Bar (with separator) on iOS in light modeBottom Bar (with separator) on iOS in dark mode
import { View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"

function Page({ children }: { children: React.ReactNode }) {
  return (
    <View className="-m-4 flex-1">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">Review order</Text>
        <Text variant="muted">2 items, delivery on Thursday.</Text>
      </View>
      {children}
    </View>
  )
}

export function BottomBarWithSeparatorExample() {
  return (
    <Page>
      <BottomBar separator>
        <BottomBarContent>
          <Button size="lg" block>
            <Text>Place order</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Page>
  )
}
import EsuissUI
import SwiftUI

struct BottomBarWithSeparatorExample: View {
  var body: some View {
    BottomBarPage {
      BottomBar(separator: true) {
        BottomBarContent {
          Button("Place order") {}.buttonStyle(.variant(size: .lg, block: true))
        }
      }
    }
  }
}

struct BottomBarPage<Bar: View>: View {
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text("Review order").textVariant(.h3)
        Text("2 items, delivery on Thursday.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun BottomBarPage(page: @Composable ColumnScope.() -> Unit, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
                content = page,
            )
            bar()
        }
    }
}

@Composable
fun BottomBarOrder(bar: @Composable () -> Unit) {
    BottomBarPage(
        page = {
            Text("Review order", variant = TextVariant.H3)
            Text("2 items, delivery on Thursday.", variant = TextVariant.Muted)
        },
        bar = bar,
    )
}

@Composable
fun BottomBarWithSeparator() {
    BottomBarOrder {
        BottomBar(separator = true) {
            BottomBarContent {
                Button("Place order", onClick = {}, size = ButtonSize.Lg, block = true)
            }
        }
    }
}

Two actions

A primary and a plain button stacked.

iOS
Bottom Bar (two actions) on iOS in light modeBottom Bar (two actions) on iOS in dark mode
import { View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"

function Page({ children }: { children: React.ReactNode }) {
  return (
    <View className="-m-4 flex-1">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">Review order</Text>
        <Text variant="muted">2 items, delivery on Thursday.</Text>
      </View>
      {children}
    </View>
  )
}

export function BottomBarTwoActionsExample() {
  return (
    <Page>
      <BottomBar separator>
        <BottomBarContent>
          <Button size="lg" block>
            <Text>Place order</Text>
          </Button>
          <Button size="lg" variant="plain" block>
            <Text>Save for later</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Page>
  )
}
import EsuissUI
import SwiftUI

struct BottomBarTwoActionsExample: View {
  var body: some View {
    BottomBarPage {
      BottomBar(separator: true) {
        BottomBarContent {
          Button("Place order") {}.buttonStyle(.variant(size: .lg, block: true))
          Button("Save for later") {}.buttonStyle(.variant(.plain, size: .lg, block: true))
        }
      }
    }
  }
}

struct BottomBarPage<Bar: View>: View {
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text("Review order").textVariant(.h3)
        Text("2 items, delivery on Thursday.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.ButtonVariant
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun BottomBarPage(page: @Composable ColumnScope.() -> Unit, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
                content = page,
            )
            bar()
        }
    }
}

@Composable
fun BottomBarOrder(bar: @Composable () -> Unit) {
    BottomBarPage(
        page = {
            Text("Review order", variant = TextVariant.H3)
            Text("2 items, delivery on Thursday.", variant = TextVariant.Muted)
        },
        bar = bar,
    )
}

@Composable
fun BottomBarTwoActions() {
    BottomBarOrder {
        BottomBar(separator = true) {
            BottomBarContent {
                Button("Place order", onClick = {}, size = ButtonSize.Lg, block = true)
                Button("Save for later", onClick = {}, size = ButtonSize.Lg, variant = ButtonVariant.Plain, block = true)
            }
        }
    }
}

Side by side

Back and Next buttons in a row.

iOS
Bottom Bar (side by side) on iOS in light modeBottom Bar (side by side) on iOS in dark mode
import { View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"

function Page({ children }: { children: React.ReactNode }) {
  return (
    <View className="-m-4 flex-1">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">Review order</Text>
        <Text variant="muted">2 items, delivery on Thursday.</Text>
      </View>
      {children}
    </View>
  )
}

export function BottomBarSideBySideExample() {
  return (
    <Page>
      <BottomBar separator>
        <BottomBarContent className="flex-row">
          <Button size="lg" variant="secondary" block className="flex-1">
            <Text>Back</Text>
          </Button>
          <Button size="lg" block className="flex-1">
            <Text>Next</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Page>
  )
}
import EsuissUI
import SwiftUI

struct BottomBarSideBySideExample: View {
  var body: some View {
    BottomBarPage {
      BottomBar(separator: true) {
        BottomBarContent {
          HStack(spacing: 8) {
            Button("Back") {}.buttonStyle(.variant(.secondary, size: .lg, block: true))
            Button("Next") {}.buttonStyle(.variant(size: .lg, block: true))
          }
        }
      }
    }
  }
}

struct BottomBarPage<Bar: View>: View {
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text("Review order").textVariant(.h3)
        Text("2 items, delivery on Thursday.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.ButtonVariant
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun BottomBarPage(page: @Composable ColumnScope.() -> Unit, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
                content = page,
            )
            bar()
        }
    }
}

@Composable
fun BottomBarOrder(bar: @Composable () -> Unit) {
    BottomBarPage(
        page = {
            Text("Review order", variant = TextVariant.H3)
            Text("2 items, delivery on Thursday.", variant = TextVariant.Muted)
        },
        bar = bar,
    )
}

@Composable
fun BottomBarSideBySide() {
    BottomBarOrder {
        BottomBar(separator = true) {
            BottomBarContent {
                Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                    Button("Back", onClick = {}, modifier = Modifier.weight(1f), size = ButtonSize.Lg, variant = ButtonVariant.Secondary, block = true)
                    Button("Next", onClick = {}, modifier = Modifier.weight(1f), size = ButtonSize.Lg, block = true)
                }
            }
        }
    }
}

Above keyboard

The bar moving up with the keyboard while an input is focused.

iOS
Bottom Bar (above keyboard) on iOS in light modeBottom Bar (above keyboard) on iOS in dark mode
import { View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Text } from "@/components/ui/text"

export function BottomBarAboveKeyboardExample() {
  return (
    <View className="-m-4 flex-1">
      <View className="flex-1 gap-3 p-4">
        <Text variant="h3">Promo code</Text>
        <Input placeholder="Enter code" autoFocus autoCapitalize="characters" />
      </View>
      <BottomBar separator>
        <BottomBarContent>
          <Button size="lg" block>
            <Text>Apply</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </View>
  )
}
import EsuissUI
import SwiftUI

struct BottomBarKeyboardDemo: View {
  @State private var code = ""
  @FocusState private var focused: Bool

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 12) {
        Text("Promo code").textVariant(.h3)
        TextField("Promo code", text: $code, prompt: Text("Enter code"))
          .textFieldStyle(.input)
          .textInputAutocapitalization(.characters)
          .focused($focused)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      BottomBar(separator: true) {
        BottomBarContent {
          Button("Apply") {}.buttonStyle(.variant(size: .lg, block: true))
        }
      }
    }
    .onAppear { focused = true }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
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.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
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.ScreenEdge
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun BottomBarPage(page: @Composable ColumnScope.() -> Unit, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
                content = page,
            )
            bar()
        }
    }
}

@Composable
fun BottomBarAboveKeyboard() {
    var code by rememberSaveable { mutableStateOf("") }
    val focus = remember { FocusRequester() }
    LaunchedEffect(Unit) { focus.requestFocus() }
    BottomBarPage(
        page = {
            Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
                Text("Promo code", variant = TextVariant.H3)
                Input(
                    code,
                    { code = it },
                    Modifier.focusRequester(focus),
                    placeholder = "Enter code",
                    keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Characters),
                )
            }
        },
        bar = {
            BottomBar(separator = true) {
                BottomBarContent {
                    Button("Apply", onClick = {}, size = ButtonSize.Lg, block = true)
                }
            }
        },
    )
}

API Reference

01

BottomBar

Bottom container that follows the safe area and keyboard.

PropTypeDefaultDescription
separatorbooleanfalseDraws a top border. Defaults to false.

Also accepts the props of ViewProps.

02

BottomBarContent

Padded area for the actions.

Also accepts the props of ViewProps.

Accessibility

  • The bar adds no role; each action inside provides its own.

Notes

  • The padding animation respects the system Reduce Motion setting.
  • Uses bg-background and border-border tokens; light and dark follow the system appearance.
NextBreadcrumbPreviousBadge

On this page

  1. 01Installation
    • Manual
  2. 02Usage
  3. 03Examples
    • With separator
    • Two actions
    • Side by side
    • Above keyboard
  4. 04API Reference
    • BottomBar
    • BottomBarContent
  5. 05Accessibility
  6. 06Notes