Floating action button
Free · MITReact Native · ExpoA 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/fab.jsonThen 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
- 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.
- 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
| Prop | Type | Default |
|---|---|---|
| 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`. | boolean | true |
| 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. | number | 16 |
| safeArea Add the bottom and side safe area to `offset`. Turn off inside a tab bar or a card. Default `true`. | boolean | true |
| scrim Dim the parent behind an open speed dial; tapping it closes the dial. Default `true`. | boolean | true |
| 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.
Related
A layered icon-only button whose surface and icon sink on touch down and release on different springs; it runs async actions itself, carries a badge and shows a hint on long press.
A press-responsive button with haptics that runs async actions on its own: it gathers into a spinner, then opens into a success or error state.
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.