suiss/uim · React Native Component
Fab
A floating action button pinned to the bottom corner, with an optional label and speed-dial actions.
Fab 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 { Fab } from "@/components/ui/fab"
import { Text } from "@/components/ui/text"
function Page({ log }: { log?: string }) {
return (
<View className="gap-2">
<Text variant="h3">Notes</Text>
<Text variant="muted">
The button floats at the bottom end of the screen.
</Text>
{log ? (
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
) : null}
</View>
)
}
export function FabExample() {
return (
<View className="flex-1">
<Page />
<Fab accessibilityLabel="New note" />
</View>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct FabExample: View {
var body: some View {
FabStage {
Fab(accessibilityLabel: "New note", action: {}) { Ph.plus.bold }
}
}
}
struct FabPage: View {
var log: String?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Notes")
.textVariant(.h3)
Text("The button floats at the bottom end of the screen.")
.textVariant(.muted)
if let log, !log.isEmpty {
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct FabStage<Content: View>: View {
var log: String?
@ViewBuilder let content: () -> Content
var body: some View {
ZStack(alignment: .topLeading) {
FabPage(log: log)
content()
}
.containerRelativeFrame(.vertical) { height, _ in max(height - 32, 0) }
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.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.Plus
import com.esuiss.ui.components.Fab
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun FabExample() {
FabPage {
Fab(PhosphorIcons.Regular.Plus, contentDescription = "New note", onClick = {})
}
}
@Composable
fun FabPage(log: String? = null, fab: @Composable () -> Unit) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Notes", variant = TextVariant.H3)
Text("The button floats at the bottom end of the screen.", variant = TextVariant.Muted)
if (!log.isNullOrEmpty()) {
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}
fab()
}
}Fab is a round accent Pressable rendered through a portal and pinned to the bottom-end corner of the screen, above the safe-area insets. It comes in default and lg sizes, and becomes an extended pill when you pass a label.
When it has FabAction children, pressing it opens a speed dial: a scrim dims the screen and the actions rise above the button one after another. The plus icon rotates 45 degrees into a close mark; other icons swap to an X.
Installation
$ pnpm dlx @esuiss/uim@latest add fab$ pnpm dlx @esuiss/uim@latest add fab --platform swift$ pnpm dlx @esuiss/uim@latest add fab --platform kotlinThe CLI also adds what it builds on: Icon, Text.
Manual
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Icon, Text, and the primitives
components/primitives/hooks.tsx,components/primitives/portal.tsx. -
Copy the source into
components/ui/fab.tsx:
import { cn } from "cn"
import {
type Icon as PhosphorIcon,
PlusIcon,
XIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, View } from "react-native"
import {
Easing,
FadeIn,
FadeInDown,
FadeOut,
FadeOutDown,
ReduceMotion,
useAnimatedStyle,- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Icon, Text, and the primitives
components/primitives/hooks.tsx,components/primitives/portal.tsx. -
Copy the source into
components/ui/fab.tsx:
import { cn } from "cn"
import {
type Icon as PhosphorIcon,
PlusIcon,
XIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, View } from "react-native"
import {
Easing,
FadeIn,
FadeInDown,
FadeOut,
FadeOutDown,
ReduceMotion,
useAnimatedStyle,- Update the import paths to match your project.
- Install the dependencies:
npx expo install phosphor-react-native react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Icon, Text, and the primitives
components/primitives/hooks.tsx,components/primitives/portal.tsx. -
Copy the source into
components/ui/fab.tsx:
import {
type Icon as PhosphorIcon,
PlusIcon,
XIcon,
} from "phosphor-react-native"
import * as React from "react"
import {
I18nManager,
Pressable,
type StyleProp,
StyleSheet,
View,
type ViewStyle,
} from "react-native"
import {
Easing,- 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 PhosphorSwift
import SwiftUI
public enum FabSize: CaseIterable, Sendable {
case `default`, lg
var dimension: CGFloat {
switch self {
case .default: 56
case .lg: 64
}
}
var iconSize: CGFloat {
switch self {
case .default: 24import 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")
implementation("com.adamglin:phosphor-icon-android:1.0.0")- Copy the theme files every view uses:
package com.esuiss.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffsetpackage com.esuiss.ui.theme
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Immutable
data class Colors(
val surface: Color,
val surfaceSecondary: Color,
val surfaceTertiary: Color,
val surfaceRaised: Color,
val label: Color,
val labelSecondary: Color,
val labelTertiary: Color,
val labelQuaternary: Color,- Copy the source and every file it uses:
package com.esuiss.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSourcepackage 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 { Fab, FabAction } from "@/components/ui/fab"<View className="flex-1">
<Page />
<Fab accessibilityLabel="New note" />
</View>Use the default plus icon or pass any Phosphor icon to icon. Give an icon-only button an accessibilityLabel.
For a speed dial, add FabAction children with an icon and an optional text label. Pressing an action calls its onPress and closes the dial. Tapping the scrim or pressing the Android back button also closes it. Use open, defaultOpen and onOpenChange to control it.
Pass portalHost to render into a named portal host.
Examples
Large
The lg size with a custom icon.


import { PencilSimpleIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Fab } from "@/components/ui/fab"
import { Text } from "@/components/ui/text"
function Page({ log }: { log?: string }) {
return (
<View className="gap-2">
<Text variant="h3">Notes</Text>
<Text variant="muted">
The button floats at the bottom end of the screen.
</Text>
{log ? (
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
) : null}
</View>
)
}
export function FabLargeExample() {
return (
<View className="flex-1">
<Page />
<Fab size="lg" icon={PencilSimpleIcon} accessibilityLabel="Compose" />
</View>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct FabLargeExample: View {
var body: some View {
FabStage {
Fab(accessibilityLabel: "Compose", size: .lg, action: {}) { Ph.pencilSimple.bold }
}
}
}
struct FabPage: View {
var log: String?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Notes")
.textVariant(.h3)
Text("The button floats at the bottom end of the screen.")
.textVariant(.muted)
if let log, !log.isEmpty {
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct FabStage<Content: View>: View {
var log: String?
@ViewBuilder let content: () -> Content
var body: some View {
ZStack(alignment: .topLeading) {
FabPage(log: log)
content()
}
.containerRelativeFrame(.vertical) { height, _ in max(height - 32, 0) }
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.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.PencilSimple
import com.esuiss.ui.components.Fab
import com.esuiss.ui.components.FabSize
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun FabLargeExample() {
FabPage {
Fab(PhosphorIcons.Regular.PencilSimple, contentDescription = "Compose", onClick = {}, size = FabSize.Lg)
}
}
@Composable
fun FabPage(log: String? = null, fab: @Composable () -> Unit) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Notes", variant = TextVariant.H3)
Text("The button floats at the bottom end of the screen.", variant = TextVariant.Muted)
if (!log.isNullOrEmpty()) {
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}
fab()
}
}Extended
A label beside the icon.


import { PencilSimpleIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Fab } from "@/components/ui/fab"
import { Text } from "@/components/ui/text"
function Page({ log }: { log?: string }) {
return (
<View className="gap-2">
<Text variant="h3">Notes</Text>
<Text variant="muted">
The button floats at the bottom end of the screen.
</Text>
{log ? (
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
) : null}
</View>
)
}
export function FabExtendedExample() {
return (
<View className="flex-1">
<Page />
<Fab icon={PencilSimpleIcon} label="Compose" />
</View>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct FabExtendedExample: View {
var body: some View {
FabStage {
Fab("Compose", action: {}) { Ph.pencilSimple.bold }
}
}
}
struct FabPage: View {
var log: String?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Notes")
.textVariant(.h3)
Text("The button floats at the bottom end of the screen.")
.textVariant(.muted)
if let log, !log.isEmpty {
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct FabStage<Content: View>: View {
var log: String?
@ViewBuilder let content: () -> Content
var body: some View {
ZStack(alignment: .topLeading) {
FabPage(log: log)
content()
}
.containerRelativeFrame(.vertical) { height, _ in max(height - 32, 0) }
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.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.PencilSimple
import com.esuiss.ui.components.Fab
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun FabExtendedExample() {
FabPage {
Fab(PhosphorIcons.Regular.PencilSimple, contentDescription = "Compose", onClick = {}, label = "Compose")
}
}
@Composable
fun FabPage(log: String? = null, fab: @Composable () -> Unit) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Notes", variant = TextVariant.H3)
Text("The button floats at the bottom end of the screen.", variant = TextVariant.Muted)
if (!log.isNullOrEmpty()) {
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}
fab()
}
}Large extended
A large extended button.


import { View } from "react-native"
import { Fab } from "@/components/ui/fab"
import { Text } from "@/components/ui/text"
function Page({ log }: { log?: string }) {
return (
<View className="gap-2">
<Text variant="h3">Notes</Text>
<Text variant="muted">
The button floats at the bottom end of the screen.
</Text>
{log ? (
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
) : null}
</View>
)
}
export function FabLargeExtendedExample() {
return (
<View className="flex-1">
<Page />
<Fab size="lg" label="New note" />
</View>
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct FabLargeExtendedExample: View {
var body: some View {
FabStage {
Fab("New note", size: .lg, action: {}) { Ph.plus.bold }
}
}
}
struct FabPage: View {
var log: String?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Notes")
.textVariant(.h3)
Text("The button floats at the bottom end of the screen.")
.textVariant(.muted)
if let log, !log.isEmpty {
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct FabStage<Content: View>: View {
var log: String?
@ViewBuilder let content: () -> Content
var body: some View {
ZStack(alignment: .topLeading) {
FabPage(log: log)
content()
}
.containerRelativeFrame(.vertical) { height, _ in max(height - 32, 0) }
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.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.Plus
import com.esuiss.ui.components.Fab
import com.esuiss.ui.components.FabSize
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun FabLargeExtendedExample() {
FabPage {
Fab(PhosphorIcons.Regular.Plus, contentDescription = "New note", onClick = {}, size = FabSize.Lg, label = "New note")
}
}
@Composable
fun FabPage(log: String? = null, fab: @Composable () -> Unit) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Notes", variant = TextVariant.H3)
Text("The button floats at the bottom end of the screen.", variant = TextVariant.Muted)
if (!log.isNullOrEmpty()) {
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}
fab()
}
}Speed dial open
A speed dial that starts open.


import { CameraIcon, FileTextIcon, ImageIcon, NotePencilIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Fab, FabAction } from "@/components/ui/fab"
import { Text } from "@/components/ui/text"
function Page({ log }: { log?: string }) {
return (
<View className="gap-2">
<Text variant="h3">Notes</Text>
<Text variant="muted">
The button floats at the bottom end of the screen.
</Text>
{log ? (
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
) : null}
</View>
)
}
function SpeedDial({ defaultOpen = false }: { defaultOpen?: boolean }) {
const [log, setLog] = React.useState("")
return (
<View className="flex-1">
<Page log={log} />
<Fab accessibilityLabel="Create" defaultOpen={defaultOpen}>
<FabAction icon={NotePencilIcon} onPress={() => setLog("New note")}>
New note
</FabAction>
<FabAction icon={CameraIcon} onPress={() => setLog("Take photo")}>
Take photo
</FabAction>
<FabAction icon={ImageIcon} onPress={() => setLog("Add image")}>
Add image
</FabAction>
<FabAction icon={FileTextIcon} onPress={() => setLog("Scan document")}>
Scan document
</FabAction>
</Fab>
</View>
)
}
export function FabSpeedDialOpenExample() {
return (
<SpeedDial defaultOpen />
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct FabSpeedDialOpenExample: View {
var body: some View {
FabSpeedDial(open: true)
}
}
struct FabPage: View {
var log: String?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Notes")
.textVariant(.h3)
Text("The button floats at the bottom end of the screen.")
.textVariant(.muted)
if let log, !log.isEmpty {
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct FabStage<Content: View>: View {
var log: String?
@ViewBuilder let content: () -> Content
var body: some View {
ZStack(alignment: .topLeading) {
FabPage(log: log)
content()
}
.containerRelativeFrame(.vertical) { height, _ in max(height - 32, 0) }
}
}
struct FabSpeedDial: View {
@State private var open: Bool
@State private var log = ""
init(open: Bool) {
_open = State(initialValue: open)
}
var body: some View {
FabStage(log: log) {
Fab(accessibilityLabel: "Create", isExpanded: $open) {
FabAction("New note", action: { log = "New note" }) { Ph.notePencil.regular }
FabAction("Take photo", action: { log = "Take photo" }) { Ph.camera.regular }
FabAction("Add image", action: { log = "Add image" }) { Ph.image.regular }
FabAction("Scan document", action: { log = "Scan document" }) { Ph.fileText.regular }
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.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.Camera
import com.adamglin.phosphoricons.regular.FileText
import com.adamglin.phosphoricons.regular.Image
import com.adamglin.phosphoricons.regular.NotePencil
import com.adamglin.phosphoricons.regular.Plus
import com.esuiss.ui.components.Fab
import com.esuiss.ui.components.FabAction
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun FabSpeedDialOpenExample() {
FabSpeedDial(defaultOpen = true)
}
@Composable
fun FabPage(log: String? = null, fab: @Composable () -> Unit) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Notes", variant = TextVariant.H3)
Text("The button floats at the bottom end of the screen.", variant = TextVariant.Muted)
if (!log.isNullOrEmpty()) {
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}
fab()
}
}
@Composable
fun FabSpeedDial(defaultOpen: Boolean) {
var open by rememberSaveable { mutableStateOf(defaultOpen) }
var log by rememberSaveable { mutableStateOf("") }
FabPage(log) {
Fab(PhosphorIcons.Regular.Plus, contentDescription = "Create", open = open, onOpenChange = { open = it }) {
FabAction(PhosphorIcons.Regular.NotePencil, "New note", { log = "New note" })
FabAction(PhosphorIcons.Regular.Camera, "Take photo", { log = "Take photo" })
FabAction(PhosphorIcons.Regular.Image, "Add image", { log = "Add image" })
FabAction(PhosphorIcons.Regular.FileText, "Scan document", { log = "Scan document" })
}
}
}Speed dial closed
A speed dial that opens on press.


import { CameraIcon, FileTextIcon, ImageIcon, NotePencilIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Fab, FabAction } from "@/components/ui/fab"
import { Text } from "@/components/ui/text"
function Page({ log }: { log?: string }) {
return (
<View className="gap-2">
<Text variant="h3">Notes</Text>
<Text variant="muted">
The button floats at the bottom end of the screen.
</Text>
{log ? (
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
) : null}
</View>
)
}
function SpeedDial({ defaultOpen = false }: { defaultOpen?: boolean }) {
const [log, setLog] = React.useState("")
return (
<View className="flex-1">
<Page log={log} />
<Fab accessibilityLabel="Create" defaultOpen={defaultOpen}>
<FabAction icon={NotePencilIcon} onPress={() => setLog("New note")}>
New note
</FabAction>
<FabAction icon={CameraIcon} onPress={() => setLog("Take photo")}>
Take photo
</FabAction>
<FabAction icon={ImageIcon} onPress={() => setLog("Add image")}>
Add image
</FabAction>
<FabAction icon={FileTextIcon} onPress={() => setLog("Scan document")}>
Scan document
</FabAction>
</Fab>
</View>
)
}
export function FabSpeedDialClosedExample() {
return (
<SpeedDial />
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct FabSpeedDialClosedExample: View {
var body: some View {
FabSpeedDial(open: false)
}
}
struct FabPage: View {
var log: String?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Notes")
.textVariant(.h3)
Text("The button floats at the bottom end of the screen.")
.textVariant(.muted)
if let log, !log.isEmpty {
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct FabStage<Content: View>: View {
var log: String?
@ViewBuilder let content: () -> Content
var body: some View {
ZStack(alignment: .topLeading) {
FabPage(log: log)
content()
}
.containerRelativeFrame(.vertical) { height, _ in max(height - 32, 0) }
}
}
struct FabSpeedDial: View {
@State private var open: Bool
@State private var log = ""
init(open: Bool) {
_open = State(initialValue: open)
}
var body: some View {
FabStage(log: log) {
Fab(accessibilityLabel: "Create", isExpanded: $open) {
FabAction("New note", action: { log = "New note" }) { Ph.notePencil.regular }
FabAction("Take photo", action: { log = "Take photo" }) { Ph.camera.regular }
FabAction("Add image", action: { log = "Add image" }) { Ph.image.regular }
FabAction("Scan document", action: { log = "Scan document" }) { Ph.fileText.regular }
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.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.Camera
import com.adamglin.phosphoricons.regular.FileText
import com.adamglin.phosphoricons.regular.Image
import com.adamglin.phosphoricons.regular.NotePencil
import com.adamglin.phosphoricons.regular.Plus
import com.esuiss.ui.components.Fab
import com.esuiss.ui.components.FabAction
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun FabSpeedDialClosedExample() {
FabSpeedDial(defaultOpen = false)
}
@Composable
fun FabPage(log: String? = null, fab: @Composable () -> Unit) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Notes", variant = TextVariant.H3)
Text("The button floats at the bottom end of the screen.", variant = TextVariant.Muted)
if (!log.isNullOrEmpty()) {
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}
fab()
}
}
@Composable
fun FabSpeedDial(defaultOpen: Boolean) {
var open by rememberSaveable { mutableStateOf(defaultOpen) }
var log by rememberSaveable { mutableStateOf("") }
FabPage(log) {
Fab(PhosphorIcons.Regular.Plus, contentDescription = "Create", open = open, onOpenChange = { open = it }) {
FabAction(PhosphorIcons.Regular.NotePencil, "New note", { log = "New note" })
FabAction(PhosphorIcons.Regular.Camera, "Take photo", { log = "Take photo" })
FabAction(PhosphorIcons.Regular.Image, "Add image", { log = "Add image" })
FabAction(PhosphorIcons.Regular.FileText, "Scan document", { log = "Scan document" })
}
}
}Controlled
A speed dial controlled with open and onOpenChange.


import { CameraIcon, NotePencilIcon, PencilSimpleIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Fab, FabAction } from "@/components/ui/fab"
import { Text } from "@/components/ui/text"
function Page({ log }: { log?: string }) {
return (
<View className="gap-2">
<Text variant="h3">Notes</Text>
<Text variant="muted">
The button floats at the bottom end of the screen.
</Text>
{log ? (
<Text role="status" className="text-sm text-label-secondary">
{log}
</Text>
) : null}
</View>
)
}
function ControlledDemo() {
const [open, setOpen] = React.useState(true)
return (
<View className="flex-1">
<Page log={open ? "Open" : "Closed"} />
<Fab
icon={PencilSimpleIcon}
label="Create"
open={open}
onOpenChange={setOpen}
>
<FabAction icon={NotePencilIcon}>New note</FabAction>
<FabAction icon={CameraIcon}>Take photo</FabAction>
</Fab>
</View>
)
}
export function FabControlledExample() {
return (
<ControlledDemo />
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct FabPage: View {
var log: String?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Notes")
.textVariant(.h3)
Text("The button floats at the bottom end of the screen.")
.textVariant(.muted)
if let log, !log.isEmpty {
Text(verbatim: log)
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct FabStage<Content: View>: View {
var log: String?
@ViewBuilder let content: () -> Content
var body: some View {
ZStack(alignment: .topLeading) {
FabPage(log: log)
content()
}
.containerRelativeFrame(.vertical) { height, _ in max(height - 32, 0) }
}
}
struct FabControlled: View {
@State private var open = true
var body: some View {
FabStage(log: open ? "Open" : "Closed") {
Fab("Create", isExpanded: $open) {
Ph.pencilSimple.bold
} actions: {
FabAction("New note", action: {}) { Ph.notePencil.regular }
FabAction("Take photo", action: {}) { Ph.camera.regular }
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.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.Camera
import com.adamglin.phosphoricons.regular.NotePencil
import com.adamglin.phosphoricons.regular.PencilSimple
import com.esuiss.ui.components.Fab
import com.esuiss.ui.components.FabAction
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun FabPage(log: String? = null, fab: @Composable () -> Unit) {
Box(Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Notes", variant = TextVariant.H3)
Text("The button floats at the bottom end of the screen.", variant = TextVariant.Muted)
if (!log.isNullOrEmpty()) {
Text(
log,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm.copy(color = EsuissTheme.colors.labelSecondary),
)
}
}
fab()
}
}
@Composable
fun FabControlled() {
var open by rememberSaveable { mutableStateOf(true) }
FabPage(if (open) "Open" else "Closed") {
Fab(
PhosphorIcons.Regular.PencilSimple,
contentDescription = "Create",
open = open,
onOpenChange = { open = it },
label = "Create",
) {
FabAction(PhosphorIcons.Regular.NotePencil, "New note", {})
FabAction(PhosphorIcons.Regular.Camera, "Take photo", {})
}
}
}API Reference
Fab
Floating button that also hosts the speed dial.
React.ComponentProps<typeof Pressable>- About
defaultorlg.- Type
FabSize- Default
- "default"
- About
- Phosphor icon component. Defaults to a plus.
- Type
PhosphorIcon- Default
- PlusIcon
- About
- Text or node shown beside the icon.
- Type
React.ReactNode- Default
- —
- About
- Controlled speed dial state.
- Type
boolean- Default
- —
- About
- Initial speed dial state.
- Type
boolean- Default
- false
- About
- Called when the speed dial opens or closes.
- Type
(open: boolean) => void- Default
- —
- About
- Name of the portal host to render into.
- Type
string- Default
- —
- Type
React.ReactNode- Default
- —
FabAction
One action in the speed dial, with an icon and optional label.
React.ComponentProps<typeof Pressable>- About
- Phosphor icon component for the action.
- Type
PhosphorIcon- Default
- required
- Type
React.ReactNode- Default
- —
Accessibility
- The button and each action have the
buttonrole. - With actions, the button reports its
expandedstate. - The VoiceOver escape gesture and the Android back button close the speed dial.
- The scrim is hidden from screen readers.
Notes
- Position follows the safe-area insets and flips sides in right-to-left layouts.
- Animations respect the system Reduce Motion setting.
- Uses
bg-accent,bg-surface-raisedandbg-scrimtokens; actions use a secondary surface in dark mode.