Floating action button

Free · MITReact Native · Expo

A floating main action that opens a speed dial (the plus turns into a close mark, a scrim dims the screen, mini actions rise one by one with labels) and, extended, folds its label away while you scroll.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/fab.json

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/fab.

Usage

import { FlatList, View } from "react-native";
import { Fab, useFabExtended } from "@/components/goodui-native/fab/fab";

export function Inbox({ messages, compose }: { messages: Message[]; compose: () => void }) {
  const { extended, onScroll } = useFabExtended();
  return (
    <View style={{ flex: 1 }}>
      <FlatList data={messages} onScroll={onScroll} scrollEventThrottle={32} renderItem={renderMessage} />
      <Fab accessibilityLabel="Compose" label="Compose" extended={extended} onPress={compose} />
    </View>
  );
}

When to use

Use it for
  • One main creating action on a scrolling screen: compose, new task, add expense.
  • Two to six related create actions behind one button: transfer, request, top up, split a bill.
  • An extended FAB with a label when the action is not obvious from the icon alone.
Reach for something else
  • Screens with a natural bottom bar for the action: use button in a footer (or the bottom-sheet footer).
  • Destructive or secondary actions: they do not float.
  • More than six actions or actions with long names: use bottom-sheet with a list.
  • Toolbar icons: use icon-button.

Examples

Speed dial

<Fab
  accessibilityLabel="New"
  actions={[
    { key: "send", label: "Send money", icon: (c) => <SendIcon color={c} />, onPress: send },
    { key: "request", label: "Request", icon: (c) => <RequestIcon color={c} />, onPress: request },
    { key: "split", label: "Split a bill", icon: (c) => <SplitIcon color={c} />, onPress: split, tone: "accent" },
  ]}
/>

Inside a tab screen with its own bar

<Fab accessibilityLabel="Add expense" safeArea={false} offset={16} onPress={add} />

API reference

PropTypeDefault
accessibilityLabel*
Required: what the button does, for example "New message".
string—
icon
Defaults to a plus, which turns into a close mark when a speed dial opens.
FabIcon—
label
Text beside the icon (an extended FAB).
string—
extended
Show the label. Set it from scroll direction with `useFabExtended` so the FAB collapses while reading. Default `true`.
booleantrue
onPress
Runs on press when there are no `actions`.
() => void—
actions
Two to six actions turn the FAB into a speed dial.
FabAction[]—
open
Controlled speed dial state.
boolean—
onOpenChange
Called when the speed dial opens or closes.
(open: boolean) => void—
placement
Corner of the parent it sits in. Mirrored under RTL, as React Native does on a device (right means the end side).
"right" | "left" | "center""right"
variant
`primary` is the accent fill; `secondary` a raised surface for screens whose main action lives elsewhere.
"primary" | "secondary""primary"
offset
Distance from the parent's edges, in pt. Default 16.
number16
safeArea
Add the bottom and side safe area to `offset`. Turn off inside a tab bar or a card. Default `true`.
booleantrue
scrim
Dim the parent behind an open speed dial; tapping it closes the dial. Default `true`.
booleantrue
haptic
Haptic on press. `false` turns it off.
HapticName | false"tap"
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<FabLabels>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Press: scales to 0.94 on the snappy spring with an overlay, from touch down.
  • Open: the default plus rotates 45° into a close mark on the morph spring (a custom icon turns and crossfades into a close mark); the scrim fades in with it.
  • Mini actions rise 16 pt and scale from 0.6 on the morph spring, nearest first, 40 ms apart; labels slide in from the side. Closing, the farthest leaves first.
  • Extended: the label slot opens and closes its real width on the morph spring; the label fades in after the slot has started opening.
  • Reduced motion: no rotation, rise, scale or stagger; icons, actions and the label crossfade or change instantly.
  • Haptic: tap on press (configurable with the haptic prop)
  • Haptic: tap when a speed dial action runs
  • All motion runs on the UI thread. useFabExtended sets React state only when the scroll direction flips, never per frame.

Accessibility

  • The main button has role button with your accessibilityLabel; with actions it reports expanded and reads 'Close actions' while open.
  • Mini actions are buttons labelled with their label; they are hidden from screen readers and touch while the dial is closed.
  • The scrim is a 'Close menu' button. Android back closes an open dial before navigating.
  • The FAB is 56 pt and mini actions 44 pt, so every target meets 44 pt.

Notes for AI

  • Render Fab as the last child of a View with flex: 1 that holds the scroll view; it fills that parent with a touch-transparent layer and the scrim covers only that parent.
  • accessibilityLabel is required. Leave icon empty to get the plus that morphs into a close mark.
  • With actions, onPress is ignored: the press toggles the dial. Each action needs a unique key, a short label and an icon (a function receives the right color).
  • Wire extended with useFabExtended: pass onScroll and scrollEventThrottle={32} to the list.
  • Inside a tab screen or a card, pass safeArea={false}; the default adds the bottom safe area to offset.
  • Use one FAB per screen.