Button
Free · MITReact Native · ExpoA 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.
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/button.jsonThen run npx expo install --fix so react-native-reanimated match your Expo SDK. Files land in components/goodui-native/button.
Usage
import { Button } from "@/components/goodui-native/button/button";
export function SaveBar({ save }: { save: () => Promise<void> }) {
return (
<Button size="lg" successLabel="Saved" onPress={save}>
Save changes
</Button>
);
}When to use
Use it for
- The primary or secondary action of a screen: continue, save, send, subscribe.
- Actions backed by a request: return the promise from onPress and the button shows pending, success and error itself.
- Short labels that start with a verb.
Reach for something else
- Irreversible actions such as deleting an account: use hold-to-confirm.
- Switching between views: use segmented-control.
- Toolbar icons without a label: use a plain Pressable with an accessibilityLabel.
Examples
Variants and sizes
<Button>Continue</Button>
<Button variant="secondary">Not now</Button>
<Button variant="ghost">Learn more</Button>
<Button variant="destructive" size="sm">Delete</Button>A request that can fail
<Button variant="secondary" errorLabel="Couldn't send" onPress={() => api.sendInvite(email)}>
Send invite
</Button>API reference
| Prop | Type | Default |
|---|---|---|
| children* Button label. Sentence case, starts with a verb. | string | — |
| onPress Return a promise and the button runs the whole result on its own: it gathers into a spinner while the promise is pending, then opens into a success or error state. Anything else (or nothing) is an instant action. | () => unknown | — |
| variant `primary` is the one main action; `secondary` and `ghost` are quieter; `destructive` is a soft red. | "primary" | "secondary" | "ghost" | "destructive" | "primary" |
| size Heights 36, 48 and 56 pt. Small buttons keep a 44 pt touch target. | "sm" | "md" | "lg" | "md" |
| leading Icon before the label, 18 to 20 pt, colored to match the label. | ReactNode | — |
| disabled Dims the control and ignores presses. | boolean | false |
| successLabel Short text shown with the check after the promise resolves. Omit to show the check alone. | string | — |
| errorLabel Short text shown when the promise rejects. Same as `labels.error`. | string | — |
| haptic Haptic on press. `false` turns it off for this button. | HapticName | false | "tap" |
| accessibilityLabel Overrides the label read by screen readers. | string | — |
| accessibilityHint Describes what happens on activation, read after the label. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<ButtonLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Press: scales to 0.97 on the snappy spring and fills an overlay, both from touch down.
- Pending: the surface gathers into a circle on the morph spring; the layout never shifts.
- Result: color leads, the surface opens, then the check draws itself or the error appears; an error also shakes.
- Reduced motion: no scale, no shape change, no shake; states crossfade.
- Haptic: tap on press (configurable with the haptic prop)
- Haptic: success when the promise resolves
- Haptic: error when it rejects
- All animation runs on the UI thread through Reanimated shared values; React renders only on state changes.
Accessibility
- accessibilityRole is button; disabled and busy are reported through accessibilityState.
- The accessibility label follows the state: 'Save changes, in progress', then the success or error label.
- Results are announced with AccessibilityInfo.announceForAccessibility.
- Heights below 44 pt get a hitSlop that brings the touch target to 44 pt.
Notes for AI
- Return the promise from onPress to get loading, success and error for free. Do not add your own spinner or isLoading state.
- The label is children and must be a string. Put icons in the leading prop.
- It stretches in a column and hugs its label in a row. Use style={{ alignSelf: 'flex-start' }} to hug inside a column.
- Use one primary button per screen; secondary and ghost for the rest.
Related
Hold to confirm
Confirms an irreversible action by holding: ink fills from the leading edge, the label inverts where the ink passes, and haptics tick at every quarter.
Segmented control
Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.
Toast
Background confirmations that stack at the top, fan out on tap, swipe away up or sideways, and pause their timers while touched.