suiss/uim · Interaction
Top Banner
Short banners that slide in from the top of the screen, queue one at a time and can be swiped up to dismiss.
Top Banner is an open-source mobile interaction 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 { WifiSlashIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TopBannerProvider, useTopBanner } from "@/components/interactions/top-banner"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"
function Triggers() {
const { show, dismiss } = useTopBanner()
const [log, setLog] = React.useState("")
return (
<View className="w-full gap-3">
<Button
variant="outline"
onPress={() =>
show({
title: "Message sent",
description: "Alex will see it the next time they open the app.",
})
}
>
<Text>Show banner</Text>
</Button>
<Button
variant="outline"
onPress={() =>
show({
icon: WifiSlashIcon,
title: "You are offline",
description: "Changes will sync when you reconnect.",
action: { label: "Retry", onPress: () => setLog("Retry pressed") },
})
}
>
<Text>Show with icon and action</Text>
</Button>
<Button
variant="outline"
onPress={() => {
show({ title: "First of three" })
show({ title: "Second of three" })
show({ title: "Third of three" })
}}
>
<Text>Queue three</Text>
</Button>
<Button variant="ghost" onPress={() => dismiss()}>
<Text>Dismiss current</Text>
</Button>
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
</View>
)
}
export function TopBannerExample() {
return (
<TopBannerProvider>
<Triggers />
</TopBannerProvider>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct TopBannerExample: View {
var body: some View {
TopBannerStage(timeout: .seconds(4))
}
}
struct TopBannerStage: View {
var timeout: Duration = .seconds(600)
var onMount: (@MainActor (TopBannerManager) -> Void)?
@State private var manager = TopBannerManager()
@State private var shown = false
init(timeout: Duration = .seconds(600), onMount: (@MainActor (TopBannerManager) -> Void)? = nil) {
self.timeout = timeout
self.onMount = onMount
}
var body: some View {
TopBannerTriggers(manager: manager)
.frame(maxHeight: .infinity, alignment: .top)
.containerRelativeFrame(.vertical) { height, _ in height - 32 }
.topBanner(manager, timeout: timeout)
.onAppear {
guard !shown else { return }
shown = true
manager.timeout = timeout
onMount?(manager)
}
}
}
struct TopBannerTriggers: View {
let manager: TopBannerManager
@State private var log = ""
var body: some View {
VStack(spacing: 12) {
Button {
manager.show("Message sent", description: "Alex will see it the next time they open the app.")
} label: {
Text("Show banner").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show(
"You are offline",
description: "Changes will sync when you reconnect.",
icon: Ph.wifiSlash.regular,
action: TopBannerAction("Retry") { log = "Retry pressed" }
)
} label: {
Text("Show with icon and action").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show("First of three")
manager.show("Second of three")
manager.show("Third of three")
} label: {
Text("Queue three").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.dismiss()
} label: {
Text("Dismiss current").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.ghost))
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityAddTraits(.updatesFrequently)
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.WifiSlash
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.TopBannerAction
import com.esuiss.ui.interactions.TopBannerProvider
import com.esuiss.ui.interactions.TopBannerState
import com.esuiss.ui.interactions.rememberTopBannerState
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun TopBannerExample() {
TopBannerDemo(timeout = 4000L)
}
@Composable
fun TopBannerDemo(timeout: Long = 600000L, onMount: (TopBannerState.() -> Unit)? = null) {
val state = rememberTopBannerState(defaultTimeoutMillis = timeout)
var shown by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (shown || onMount == null) return@LaunchedEffect
shown = true
state.onMount()
}
TopBannerProvider(state) {
TopBannerTriggers(state)
}
}
@Composable
fun TopBannerTriggers(state: TopBannerState) {
var log by rememberSaveable { mutableStateOf("") }
val fill = Modifier.fillMaxWidth()
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
"Show banner",
onClick = { state.show("Message sent", description = "Alex will see it the next time they open the app.") },
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Show with icon and action",
onClick = {
state.show(
"You are offline",
description = "Changes will sync when you reconnect.",
icon = PhosphorIcons.Regular.WifiSlash,
action = TopBannerAction("Retry") { log = "Retry pressed" },
)
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Queue three",
onClick = {
state.show("First of three")
state.show("Second of three")
state.show("Third of three")
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button("Dismiss current", onClick = { state.dismiss() }, modifier = fill, variant = ButtonVariant.Ghost)
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}TopBannerProvider shows banners requested with show from useTopBanner. Each banner slides down below the top safe area with a title, an optional description, icon and action button, and hides after 4 seconds by default.
Banners queue and appear one at a time, with a short pause between them. Swiping a banner up past a small distance, or flicking it up, dismisses it; dragging pauses the timer. Use it for brief confirmations and status updates, such as a copied link or a restored connection.
Installation
$ pnpm dlx @esuiss/uim@latest add top-banner$ pnpm dlx @esuiss/uim@latest add top-banner --platform swift$ pnpm dlx @esuiss/uim@latest add top-banner --platform kotlinThe CLI also adds what it builds on: Button, Icon, Text.
Manual
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Button, Icon, Text, and the primitives
components/primitives/portal.tsx. -
Copy the source into
components/interactions/top-banner.tsx:
import { cn } from "cn"
import type { Icon as PhosphorIcon } from "phosphor-react-native"
import * as React from "react"
import { AccessibilityInfo, useWindowDimensions, View } from "react-native"
import { Gesture, GestureDetector } from "react-native-gesture-handler"
import {
Easing,
Keyframe,
ReduceMotion,
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Button, Icon, Text, and the primitives
components/primitives/portal.tsx. -
Copy the source into
components/interactions/top-banner.tsx:
import { cn } from "cn"
import type { Icon as PhosphorIcon } from "phosphor-react-native"
import * as React from "react"
import { AccessibilityInfo, useWindowDimensions, View } from "react-native"
import { Gesture, GestureDetector } from "react-native-gesture-handler"
import {
Easing,
Keyframe,
ReduceMotion,
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"- Update the import paths to match your project.
- Install the dependencies:
npx expo install phosphor-react-native react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Button, Icon, Text, and the primitives
components/primitives/portal.tsx. -
Copy the source into
components/interactions/top-banner.tsx:
import type { Icon as PhosphorIcon } from "phosphor-react-native"
import * as React from "react"
import {
AccessibilityInfo,
type StyleProp,
StyleSheet,
type TextStyle,
useWindowDimensions,
View,
type ViewStyle,
} from "react-native"
import { Gesture, GestureDetector } from "react-native-gesture-handler"
import {
Easing,
Keyframe,
ReduceMotion,- Update the import paths to match your project.
-
Target iOS 17 or later.
-
Add the Swift package to your app:
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")- Copy the theme files every view uses:
import SwiftUI
import UIKit
struct RGBA: Sendable {
let red: CGFloat
let green: CGFloat
let blue: CGFloat
let alpha: CGFloat
init(_ hex: UInt32, _ alpha: CGFloat = 1) {
red = CGFloat((hex >> 16) & 0xFF) / 255
green = CGFloat((hex >> 8) & 0xFF) / 255
blue = CGFloat(hex & 0xFF) / 255
self.alpha = alpha
}import SwiftUI
extension Color {
public static let surface = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
public static let surfaceSecondary = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
public static let surfaceTertiary = Color.adaptive(light: .init(0xE8E8ED), dark: .init(0x2C2C2E))
public static let surfaceRaised = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
public static let label = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let labelSecondary = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
public static let labelTertiary = Color.adaptive(light: .init(0x86868B), dark: .init(0x86868B))
public static let labelQuaternary = Color.adaptive(light: .init(0xD2D2D7), dark: .init(0x424245))
public static let control = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
public static let controlPressed = Color.adaptive(light: .init(0x000000, 0.12), dark: .init(0xFFFFFF, 0.22))
public static let controlSelected = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let controlDisabled = Color.adaptive(light: .init(0x000000, 0.03), dark: .init(0xFFFFFF, 0.05))
public static let onControlSelected = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))- Copy the source and every file it uses:
import Observation
import PhosphorSwift
import SwiftUI
public struct TopBannerAction: Sendable {
public let title: LocalizedStringResource
public let action: @MainActor @Sendable () -> Void
public init(_ title: LocalizedStringResource, action: @escaping @MainActor @Sendable () -> Void) {
self.title = title
self.action = action
}
}
public struct TopBannerItem: Identifiable, @unchecked Sendable {
public let id: UUIDimport SwiftUI
public struct VariantButtonStyle: ButtonStyle {
public enum Variant: CaseIterable, Sendable {
case `default`, outline, secondary, tinted, ghost, plain, destructive, link
}
public enum Size: CaseIterable, Sendable {
case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl
var height: CGFloat {
switch self {
case .xs, .iconXs: 24
case .sm, .iconSm: 32
case .default, .icon: 36
case .lg, .iconLg: 44import SwiftUI
extension EnvironmentValues {
@Entry var buttonGroup: Axis? = nil
@Entry var buttonGroupSize: CGSize = .zero
}
enum ButtonGroupSpace: Hashable {
case group
}
struct ButtonGroupFrame {
let bounds: Anchor<CGRect>
let outlined: Bool
}import PhosphorSwift
import SwiftUI
extension Shadow {
static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}
struct SelectionIndicator<Mark: View>: View {
let checked: Bool
let pressed: Bool
let cornerRadius: CGFloat
let mark: Mark
@Environment(\.isEnabled) private var isEnabled
@Environment(\.isInvalid) private var invalidimport SwiftUI
extension EnvironmentValues {
@Entry public var isInvalid = false
@Entry var fieldInset = false
@Entry var fieldGroupInset = false
@Entry var fieldSetInset = false
}
extension View {
public func invalid(_ isInvalid: Bool = true) -> some View {
environment(\.isInvalid, isInvalid)
}
}
public enum FieldOrientation: CaseIterable, Sendable {import SwiftUI
extension EnvironmentValues {
@Entry public var hapticsEnabled = true
}
extension View {
public func haptics(_ enabled: Bool) -> some View {
environment(\.hapticsEnabled, enabled)
}
func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
}
func hapticFeedback<Trigger: Equatable>(import SwiftUI
public struct Separator: View {
let orientation: Axis
public init(_ orientation: Axis = .horizontal) {
self.orientation = orientation
}
public var body: some View {
Rectangle()
.fill(Color.separator)
.frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
.frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
.accessibilityHidden(true)
}import SwiftUI
public enum TextVariant: CaseIterable, Sendable {
case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted
var style: TextStyle {
switch self {
case .h1: .xl3
case .h2: .xl2
case .h3: .xl
case .h4, .lead, .large: .lg
case .code, .small, .muted: .sm
default: .base
}
}- Add the files to your app target.
- Set
minSdkto 24 or higher and add the dependencies:
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")- 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.interactions
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizepackage com.esuiss.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShapepackage com.esuiss.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutablepackage com.esuiss.ui.components
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
val LocalContentStyle = compositionLocalOf { TextStyle.Default }
val LocalContentColor = compositionLocalOf { Color.Unspecified }
val LocalIconSize = compositionLocalOf { 20.dp }package com.esuiss.ui.components
import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Sizepackage 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,- Update the
com.esuiss.uipackage names to match your project.
Usage
import { TopBannerProvider } from "@/components/interactions/top-banner"<TopBannerProvider>
<Triggers />
</TopBannerProvider>Wrap the app in TopBannerProvider, then call show({ title, description?, icon?, action?, timeout? }) from useTopBanner. show returns an id that you can pass to dismiss; calling dismiss() with no id dismisses the current banner.
icon is a Phosphor icon component. action is { label, onPress }; pressing it runs onPress and dismisses the banner. Pass a timeout of 0 or Infinity to keep a banner until dismissed.
Examples
Showing
A banner with a title and description, shown on mount.


import { WifiSlashIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TopBannerProvider, useTopBanner } from "@/components/interactions/top-banner"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"
function Triggers() {
const { show, dismiss } = useTopBanner()
const [log, setLog] = React.useState("")
return (
<View className="w-full gap-3">
<Button
variant="outline"
onPress={() =>
show({
title: "Message sent",
description: "Alex will see it the next time they open the app.",
})
}
>
<Text>Show banner</Text>
</Button>
<Button
variant="outline"
onPress={() =>
show({
icon: WifiSlashIcon,
title: "You are offline",
description: "Changes will sync when you reconnect.",
action: { label: "Retry", onPress: () => setLog("Retry pressed") },
})
}
>
<Text>Show with icon and action</Text>
</Button>
<Button
variant="outline"
onPress={() => {
show({ title: "First of three" })
show({ title: "Second of three" })
show({ title: "Third of three" })
}}
>
<Text>Queue three</Text>
</Button>
<Button variant="ghost" onPress={() => dismiss()}>
<Text>Dismiss current</Text>
</Button>
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
</View>
)
}
function ShowOnMount({
options,
}: {
options: Parameters<ReturnType<typeof useTopBanner>["show"]>[0]
}) {
const { show } = useTopBanner()
const shownRef = React.useRef(false)
const optionsRef = React.useRef(options)
React.useEffect(() => {
if (shownRef.current) return
shownRef.current = true
show(optionsRef.current)
}, [show])
return <Triggers />
}
export function TopBannerShowingExample() {
return (
<TopBannerProvider timeout={600000}>
<ShowOnMount
options={{
title: "Message sent",
description: "Alex will see it the next time they open the app.",
}}
/>
</TopBannerProvider>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct TopBannerShowingExample: View {
var body: some View {
TopBannerStage { manager in
manager.show("Message sent", description: "Alex will see it the next time they open the app.")
}
}
}
struct TopBannerStage: View {
var timeout: Duration = .seconds(600)
var onMount: (@MainActor (TopBannerManager) -> Void)?
@State private var manager = TopBannerManager()
@State private var shown = false
init(timeout: Duration = .seconds(600), onMount: (@MainActor (TopBannerManager) -> Void)? = nil) {
self.timeout = timeout
self.onMount = onMount
}
var body: some View {
TopBannerTriggers(manager: manager)
.frame(maxHeight: .infinity, alignment: .top)
.containerRelativeFrame(.vertical) { height, _ in height - 32 }
.topBanner(manager, timeout: timeout)
.onAppear {
guard !shown else { return }
shown = true
manager.timeout = timeout
onMount?(manager)
}
}
}
struct TopBannerTriggers: View {
let manager: TopBannerManager
@State private var log = ""
var body: some View {
VStack(spacing: 12) {
Button {
manager.show("Message sent", description: "Alex will see it the next time they open the app.")
} label: {
Text("Show banner").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show(
"You are offline",
description: "Changes will sync when you reconnect.",
icon: Ph.wifiSlash.regular,
action: TopBannerAction("Retry") { log = "Retry pressed" }
)
} label: {
Text("Show with icon and action").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show("First of three")
manager.show("Second of three")
manager.show("Third of three")
} label: {
Text("Queue three").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.dismiss()
} label: {
Text("Dismiss current").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.ghost))
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityAddTraits(.updatesFrequently)
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.WifiSlash
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.TopBannerAction
import com.esuiss.ui.interactions.TopBannerProvider
import com.esuiss.ui.interactions.TopBannerState
import com.esuiss.ui.interactions.rememberTopBannerState
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun TopBannerShowingExample() {
TopBannerDemo { show("Message sent", description = "Alex will see it the next time they open the app.") }
}
@Composable
fun TopBannerDemo(timeout: Long = 600000L, onMount: (TopBannerState.() -> Unit)? = null) {
val state = rememberTopBannerState(defaultTimeoutMillis = timeout)
var shown by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (shown || onMount == null) return@LaunchedEffect
shown = true
state.onMount()
}
TopBannerProvider(state) {
TopBannerTriggers(state)
}
}
@Composable
fun TopBannerTriggers(state: TopBannerState) {
var log by rememberSaveable { mutableStateOf("") }
val fill = Modifier.fillMaxWidth()
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
"Show banner",
onClick = { state.show("Message sent", description = "Alex will see it the next time they open the app.") },
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Show with icon and action",
onClick = {
state.show(
"You are offline",
description = "Changes will sync when you reconnect.",
icon = PhosphorIcons.Regular.WifiSlash,
action = TopBannerAction("Retry") { log = "Retry pressed" },
)
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Queue three",
onClick = {
state.show("First of three")
state.show("Second of three")
state.show("Third of three")
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button("Dismiss current", onClick = { state.dismiss() }, modifier = fill, variant = ButtonVariant.Ghost)
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}With icon
A banner with a leading icon.
import { BellIcon, WifiSlashIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TopBannerProvider, useTopBanner } from "@/components/interactions/top-banner"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"
function Triggers() {
const { show, dismiss } = useTopBanner()
const [log, setLog] = React.useState("")
return (
<View className="w-full gap-3">
<Button
variant="outline"
onPress={() =>
show({
title: "Message sent",
description: "Alex will see it the next time they open the app.",
})
}
>
<Text>Show banner</Text>
</Button>
<Button
variant="outline"
onPress={() =>
show({
icon: WifiSlashIcon,
title: "You are offline",
description: "Changes will sync when you reconnect.",
action: { label: "Retry", onPress: () => setLog("Retry pressed") },
})
}
>
<Text>Show with icon and action</Text>
</Button>
<Button
variant="outline"
onPress={() => {
show({ title: "First of three" })
show({ title: "Second of three" })
show({ title: "Third of three" })
}}
>
<Text>Queue three</Text>
</Button>
<Button variant="ghost" onPress={() => dismiss()}>
<Text>Dismiss current</Text>
</Button>
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
</View>
)
}
function ShowOnMount({
options,
}: {
options: Parameters<ReturnType<typeof useTopBanner>["show"]>[0]
}) {
const { show } = useTopBanner()
const shownRef = React.useRef(false)
const optionsRef = React.useRef(options)
React.useEffect(() => {
if (shownRef.current) return
shownRef.current = true
show(optionsRef.current)
}, [show])
return <Triggers />
}
export function TopBannerWithIconExample() {
return (
<TopBannerProvider timeout={600000}>
<ShowOnMount
options={{
icon: BellIcon,
title: "Reminders are on",
description: "We will remind you 10 minutes before each event.",
}}
/>
</TopBannerProvider>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct TopBannerWithIconExample: View {
var body: some View {
TopBannerStage { manager in
manager.show(
"Reminders are on",
description: "We will remind you 10 minutes before each event.",
icon: Ph.bell.regular
)
}
}
}
struct TopBannerStage: View {
var timeout: Duration = .seconds(600)
var onMount: (@MainActor (TopBannerManager) -> Void)?
@State private var manager = TopBannerManager()
@State private var shown = false
init(timeout: Duration = .seconds(600), onMount: (@MainActor (TopBannerManager) -> Void)? = nil) {
self.timeout = timeout
self.onMount = onMount
}
var body: some View {
TopBannerTriggers(manager: manager)
.frame(maxHeight: .infinity, alignment: .top)
.containerRelativeFrame(.vertical) { height, _ in height - 32 }
.topBanner(manager, timeout: timeout)
.onAppear {
guard !shown else { return }
shown = true
manager.timeout = timeout
onMount?(manager)
}
}
}
struct TopBannerTriggers: View {
let manager: TopBannerManager
@State private var log = ""
var body: some View {
VStack(spacing: 12) {
Button {
manager.show("Message sent", description: "Alex will see it the next time they open the app.")
} label: {
Text("Show banner").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show(
"You are offline",
description: "Changes will sync when you reconnect.",
icon: Ph.wifiSlash.regular,
action: TopBannerAction("Retry") { log = "Retry pressed" }
)
} label: {
Text("Show with icon and action").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show("First of three")
manager.show("Second of three")
manager.show("Third of three")
} label: {
Text("Queue three").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.dismiss()
} label: {
Text("Dismiss current").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.ghost))
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityAddTraits(.updatesFrequently)
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Bell
import com.adamglin.phosphoricons.regular.WifiSlash
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.TopBannerAction
import com.esuiss.ui.interactions.TopBannerProvider
import com.esuiss.ui.interactions.TopBannerState
import com.esuiss.ui.interactions.rememberTopBannerState
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun TopBannerWithIconExample() {
TopBannerDemo {
show(
"Reminders are on",
description = "We will remind you 10 minutes before each event.",
icon = PhosphorIcons.Regular.Bell,
)
}
}
@Composable
fun TopBannerDemo(timeout: Long = 600000L, onMount: (TopBannerState.() -> Unit)? = null) {
val state = rememberTopBannerState(defaultTimeoutMillis = timeout)
var shown by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (shown || onMount == null) return@LaunchedEffect
shown = true
state.onMount()
}
TopBannerProvider(state) {
TopBannerTriggers(state)
}
}
@Composable
fun TopBannerTriggers(state: TopBannerState) {
var log by rememberSaveable { mutableStateOf("") }
val fill = Modifier.fillMaxWidth()
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
"Show banner",
onClick = { state.show("Message sent", description = "Alex will see it the next time they open the app.") },
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Show with icon and action",
onClick = {
state.show(
"You are offline",
description = "Changes will sync when you reconnect.",
icon = PhosphorIcons.Regular.WifiSlash,
action = TopBannerAction("Retry") { log = "Retry pressed" },
)
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Queue three",
onClick = {
state.show("First of three")
state.show("Second of three")
state.show("Third of three")
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button("Dismiss current", onClick = { state.dismiss() }, modifier = fill, variant = ButtonVariant.Ghost)
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}With action
A banner with an action button.


import { CloudArrowUpIcon, WifiSlashIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TopBannerProvider, useTopBanner } from "@/components/interactions/top-banner"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"
function Triggers() {
const { show, dismiss } = useTopBanner()
const [log, setLog] = React.useState("")
return (
<View className="w-full gap-3">
<Button
variant="outline"
onPress={() =>
show({
title: "Message sent",
description: "Alex will see it the next time they open the app.",
})
}
>
<Text>Show banner</Text>
</Button>
<Button
variant="outline"
onPress={() =>
show({
icon: WifiSlashIcon,
title: "You are offline",
description: "Changes will sync when you reconnect.",
action: { label: "Retry", onPress: () => setLog("Retry pressed") },
})
}
>
<Text>Show with icon and action</Text>
</Button>
<Button
variant="outline"
onPress={() => {
show({ title: "First of three" })
show({ title: "Second of three" })
show({ title: "Third of three" })
}}
>
<Text>Queue three</Text>
</Button>
<Button variant="ghost" onPress={() => dismiss()}>
<Text>Dismiss current</Text>
</Button>
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
</View>
)
}
function ShowOnMount({
options,
}: {
options: Parameters<ReturnType<typeof useTopBanner>["show"]>[0]
}) {
const { show } = useTopBanner()
const shownRef = React.useRef(false)
const optionsRef = React.useRef(options)
React.useEffect(() => {
if (shownRef.current) return
shownRef.current = true
show(optionsRef.current)
}, [show])
return <Triggers />
}
export function TopBannerWithActionExample() {
return (
<TopBannerProvider timeout={600000}>
<ShowOnMount
options={{
icon: CloudArrowUpIcon,
title: "Upload paused",
description: "3 photos are waiting for Wi-Fi.",
action: { label: "Resume", onPress: () => {} },
}}
/>
</TopBannerProvider>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct TopBannerWithActionExample: View {
var body: some View {
TopBannerStage { manager in
manager.show(
"Upload paused",
description: "3 photos are waiting for Wi-Fi.",
icon: Ph.cloudArrowUp.regular,
action: TopBannerAction("Resume") {}
)
}
}
}
struct TopBannerStage: View {
var timeout: Duration = .seconds(600)
var onMount: (@MainActor (TopBannerManager) -> Void)?
@State private var manager = TopBannerManager()
@State private var shown = false
init(timeout: Duration = .seconds(600), onMount: (@MainActor (TopBannerManager) -> Void)? = nil) {
self.timeout = timeout
self.onMount = onMount
}
var body: some View {
TopBannerTriggers(manager: manager)
.frame(maxHeight: .infinity, alignment: .top)
.containerRelativeFrame(.vertical) { height, _ in height - 32 }
.topBanner(manager, timeout: timeout)
.onAppear {
guard !shown else { return }
shown = true
manager.timeout = timeout
onMount?(manager)
}
}
}
struct TopBannerTriggers: View {
let manager: TopBannerManager
@State private var log = ""
var body: some View {
VStack(spacing: 12) {
Button {
manager.show("Message sent", description: "Alex will see it the next time they open the app.")
} label: {
Text("Show banner").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show(
"You are offline",
description: "Changes will sync when you reconnect.",
icon: Ph.wifiSlash.regular,
action: TopBannerAction("Retry") { log = "Retry pressed" }
)
} label: {
Text("Show with icon and action").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show("First of three")
manager.show("Second of three")
manager.show("Third of three")
} label: {
Text("Queue three").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.dismiss()
} label: {
Text("Dismiss current").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.ghost))
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityAddTraits(.updatesFrequently)
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CloudArrowUp
import com.adamglin.phosphoricons.regular.WifiSlash
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.TopBannerAction
import com.esuiss.ui.interactions.TopBannerProvider
import com.esuiss.ui.interactions.TopBannerState
import com.esuiss.ui.interactions.rememberTopBannerState
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun TopBannerWithActionExample() {
TopBannerDemo {
show(
"Upload paused",
description = "3 photos are waiting for Wi-Fi.",
icon = PhosphorIcons.Regular.CloudArrowUp,
action = TopBannerAction("Resume") {},
)
}
}
@Composable
fun TopBannerDemo(timeout: Long = 600000L, onMount: (TopBannerState.() -> Unit)? = null) {
val state = rememberTopBannerState(defaultTimeoutMillis = timeout)
var shown by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (shown || onMount == null) return@LaunchedEffect
shown = true
state.onMount()
}
TopBannerProvider(state) {
TopBannerTriggers(state)
}
}
@Composable
fun TopBannerTriggers(state: TopBannerState) {
var log by rememberSaveable { mutableStateOf("") }
val fill = Modifier.fillMaxWidth()
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
"Show banner",
onClick = { state.show("Message sent", description = "Alex will see it the next time they open the app.") },
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Show with icon and action",
onClick = {
state.show(
"You are offline",
description = "Changes will sync when you reconnect.",
icon = PhosphorIcons.Regular.WifiSlash,
action = TopBannerAction("Retry") { log = "Retry pressed" },
)
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Queue three",
onClick = {
state.show("First of three")
state.show("Second of three")
state.show("Third of three")
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button("Dismiss current", onClick = { state.dismiss() }, modifier = fill, variant = ButtonVariant.Ghost)
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}Title only
A banner with only a title.


import { WifiSlashIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TopBannerProvider, useTopBanner } from "@/components/interactions/top-banner"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"
function Triggers() {
const { show, dismiss } = useTopBanner()
const [log, setLog] = React.useState("")
return (
<View className="w-full gap-3">
<Button
variant="outline"
onPress={() =>
show({
title: "Message sent",
description: "Alex will see it the next time they open the app.",
})
}
>
<Text>Show banner</Text>
</Button>
<Button
variant="outline"
onPress={() =>
show({
icon: WifiSlashIcon,
title: "You are offline",
description: "Changes will sync when you reconnect.",
action: { label: "Retry", onPress: () => setLog("Retry pressed") },
})
}
>
<Text>Show with icon and action</Text>
</Button>
<Button
variant="outline"
onPress={() => {
show({ title: "First of three" })
show({ title: "Second of three" })
show({ title: "Third of three" })
}}
>
<Text>Queue three</Text>
</Button>
<Button variant="ghost" onPress={() => dismiss()}>
<Text>Dismiss current</Text>
</Button>
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
</View>
)
}
function ShowOnMount({
options,
}: {
options: Parameters<ReturnType<typeof useTopBanner>["show"]>[0]
}) {
const { show } = useTopBanner()
const shownRef = React.useRef(false)
const optionsRef = React.useRef(options)
React.useEffect(() => {
if (shownRef.current) return
shownRef.current = true
show(optionsRef.current)
}, [show])
return <Triggers />
}
export function TopBannerTitleOnlyExample() {
return (
<TopBannerProvider timeout={600000}>
<ShowOnMount options={{ title: "Copied to clipboard" }} />
</TopBannerProvider>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct TopBannerTitleOnlyExample: View {
var body: some View {
TopBannerStage { manager in
manager.show("Copied to clipboard")
}
}
}
struct TopBannerStage: View {
var timeout: Duration = .seconds(600)
var onMount: (@MainActor (TopBannerManager) -> Void)?
@State private var manager = TopBannerManager()
@State private var shown = false
init(timeout: Duration = .seconds(600), onMount: (@MainActor (TopBannerManager) -> Void)? = nil) {
self.timeout = timeout
self.onMount = onMount
}
var body: some View {
TopBannerTriggers(manager: manager)
.frame(maxHeight: .infinity, alignment: .top)
.containerRelativeFrame(.vertical) { height, _ in height - 32 }
.topBanner(manager, timeout: timeout)
.onAppear {
guard !shown else { return }
shown = true
manager.timeout = timeout
onMount?(manager)
}
}
}
struct TopBannerTriggers: View {
let manager: TopBannerManager
@State private var log = ""
var body: some View {
VStack(spacing: 12) {
Button {
manager.show("Message sent", description: "Alex will see it the next time they open the app.")
} label: {
Text("Show banner").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show(
"You are offline",
description: "Changes will sync when you reconnect.",
icon: Ph.wifiSlash.regular,
action: TopBannerAction("Retry") { log = "Retry pressed" }
)
} label: {
Text("Show with icon and action").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.show("First of three")
manager.show("Second of three")
manager.show("Third of three")
} label: {
Text("Queue three").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.outline))
Button {
manager.dismiss()
} label: {
Text("Dismiss current").frame(maxWidth: .infinity)
}
.buttonStyle(.variant(.ghost))
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityAddTraits(.updatesFrequently)
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.WifiSlash
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.TopBannerAction
import com.esuiss.ui.interactions.TopBannerProvider
import com.esuiss.ui.interactions.TopBannerState
import com.esuiss.ui.interactions.rememberTopBannerState
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun TopBannerTitleOnlyExample() {
TopBannerDemo { show("Copied to clipboard") }
}
@Composable
fun TopBannerDemo(timeout: Long = 600000L, onMount: (TopBannerState.() -> Unit)? = null) {
val state = rememberTopBannerState(defaultTimeoutMillis = timeout)
var shown by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (shown || onMount == null) return@LaunchedEffect
shown = true
state.onMount()
}
TopBannerProvider(state) {
TopBannerTriggers(state)
}
}
@Composable
fun TopBannerTriggers(state: TopBannerState) {
var log by rememberSaveable { mutableStateOf("") }
val fill = Modifier.fillMaxWidth()
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
"Show banner",
onClick = { state.show("Message sent", description = "Alex will see it the next time they open the app.") },
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Show with icon and action",
onClick = {
state.show(
"You are offline",
description = "Changes will sync when you reconnect.",
icon = PhosphorIcons.Regular.WifiSlash,
action = TopBannerAction("Retry") { log = "Retry pressed" },
)
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button(
"Queue three",
onClick = {
state.show("First of three")
state.show("Second of three")
state.show("Third of three")
},
modifier = fill,
variant = ButtonVariant.Outline,
)
Button("Dismiss current", onClick = { state.dismiss() }, modifier = fill, variant = ButtonVariant.Ghost)
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}API Reference
TopBannerProvider
Holds the banner queue and renders the current banner in a portal.
- About
- Default time in milliseconds before a banner hides. Defaults to
4000. - Type
number- Default
- 4000
- About
- Classes for the banner card.
- Type
string- Default
- —
- About
- Name of a portal host to render into.
- Type
string- Default
- —
- Type
React.ReactNode- Default
- —
Accessibility
- The banner has the
alertrole and is a polite live region. - A string title is announced when the banner appears.
- The banner has a
dismissaccessibility action.
Notes
- Banners are up to 384 points wide and centered.
- The card uses
bg-surface-raised, orbg-surface-secondaryin dark mode, with aborder-separatoroutline; the icon usestext-link. - Slide animations respect the system reduce motion setting.