Icon button
Free · MITReact Native · ExpoA 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/icon-button.jsonThen run npx expo install --fix so react-native-reanimated match your Expo SDK. Files land in components/goodui-native/icon-button.
Usage
import { IconButton } from "@/components/goodui-native/icon-button/icon-button";
import { ShareIcon, BellIcon } from "./icons";
export function ArticleHeader({ share, unread }: { share: () => Promise<void>; unread: number }) {
return (
<>
<IconButton
accessibilityLabel="Share"
icon={(color, size) => <ShareIcon color={color} size={size} />}
onPress={share}
/>
<IconButton
accessibilityLabel="Notifications"
variant="ghost"
badge={unread}
icon={(color, size) => <BellIcon color={color} size={size} />}
/>
</>
);
}When to use
- Toolbars, headers and cards where the icon is universally understood: share, more, close, bookmark, mute.
- Toggles that keep their icon, such as bookmark or mute (pass `selected`).
- Icons that carry an unread count (pass `badge`).
- Quick actions backed by a request: return the promise from onPress.
- An action whose icon is ambiguous: use button with a text label.
- The screen's floating main action: use fab.
- Liking or favoriting with a count: use like-button.
- Several mutually exclusive icons: use segmented-control.
Examples
Variants, tones and shapes
<IconButton accessibilityLabel="Add" variant="solid" tone="accent" icon={plus} />
<IconButton accessibilityLabel="Edit" variant="soft" icon={pencil} />
<IconButton accessibilityLabel="Filter" variant="outline" shape="square" icon={sliders} />
<IconButton accessibilityLabel="Delete" variant="soft" tone="danger" icon={trash} />A toggle
<IconButton
accessibilityLabel="Save to collection"
selected={saved}
onPress={() => setSaved((s) => !s)}
haptic="select"
icon={(color, size) => <BookmarkIcon color={color} size={size} filled={saved} />}
/>Long press opens a menu instead of the hint
<IconButton accessibilityLabel="More" icon={dots} onPress={openMenu} onLongPress={openContextMenu} />API reference
| Prop | Type | Default |
|---|---|---|
| icon* The icon. Pass a function to get the right color for every variant, tone and the selected state. | IconButtonIcon | — |
| accessibilityLabel* Required: an icon has no text. Also the long-press hint unless `hint` says otherwise. | string | — |
| onPress Return a promise and the icon gives way to a spinner, then a drawn check or an error mark with a shake. Anything else is an instant action. | () => unknown | — |
| onLongPress Replaces the long-press hint with your own action, for example a context menu. | () => void | — |
| hint Text of the bubble shown on long press (and after a short hover on the web). `false` turns it off. | string | false | — |
| variant `solid` for the one main action, `soft` filled quietly, `outline` and `ghost` for toolbars. | "solid" | "soft" | "outline" | "ghost" | "soft" |
| tone Color family. `neutral` follows the foreground; status tones only for status. | "success" | "warning" | "danger" | "accent" | "neutral" | "neutral" |
| size 32, 40 or 52 pt. Small ones keep a 44 pt touch target. | "sm" | "md" | "lg" | "md" |
| shape Circle or rounded square. | "round" | "square" | "round" |
| selected For toggles such as bookmark or mute: fills a non-solid button with the solid look. | boolean | — |
| badge A count, or `true` for a dot, attached to the top corner. | number | boolean | — |
| badgeMax Highest count shown as a number. Default 99. | number | 99 |
| badgeRing Surface the badge ring cuts out of. Match the surface the button sits on. | BadgeRing | false | "surface" |
| disabled Dims the control and ignores presses. | boolean | false |
| haptic Haptic on press. `false` turns it off for this button. | HapticName | false | "tap" |
| 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<IconButtonLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Touch down: the surface scales to 0.9 and the icon to 0.88 inside it on the snappy spring; on release the surface returns on snappy and the icon on morph, a beat later, so the press has depth.
- A press overlay fades in on touch down and out on release.
- selected crossfades a solid fill and the icon color (when icon is a function) with a timing.
- Async: the icon fades to a spinner, then a check draws itself or an alert mark appears with a shake.
- Long press (or a 500 ms hover on the web): a hint bubble rises 6 pt and scales in on the morph spring; it leaves 1.2 s after release or when the pointer leaves.
- Reduced motion: no press scale, no shake; the hint and states only fade.
- Haptic: tap on press (configurable with the haptic prop; use 'select' for toggles)
- Haptic: tap when onLongPress runs
- Haptic: success when the promise resolves, error when it rejects
- Press, selected and hint animations run on the UI thread; React renders on status, hint visibility and prop changes only.
Accessibility
- accessibilityLabel is required; the role is button and disabled, busy and selected (for non-solid variants) are reported in accessibilityState.
- A badge count is added to the label ('Notifications, 3 new', 'more than 99 new') and the badge itself is hidden from screen readers.
- The label follows async state: 'Share, in progress', then 'done' or 'failed', and the result is announced.
- Sizes below 44 pt get a hitSlop that brings the touch target to 44 pt.
- The long-press hint repeats the label visually and is hidden from screen readers, which already read the label.
- With `selected` set, the web reports it as a toggle (aria-pressed).
Notes for AI
- Always pass accessibilityLabel. Pass icon as a function (color, size) => node so the icon gets the right color in every variant and when selected.
- Default look is variant='soft' with tone='neutral'. Use variant='solid' tone='accent' only for the screen's one main icon action.
- Return the promise from onPress for spinner, success and error; do not add your own loading state.
- badge takes a number (hidden at 0) or true for a dot. Match badgeRing to the surface behind the button.
- The hint uses the accessibilityLabel unless you pass hint; hint={false} turns it off.
- It hugs its size (alignSelf flex-start). Put several in a row with gap 8.
Related
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 count, dot or status badge that springs in when it appears, pops when its value changes, caps at 99+ and shrinks away at zero; BadgeAnchor pins it to an icon or avatar.
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.
A like toggle with one moment of delight: the heart squashes and springs back filled, a ring expands and particles burst while the count rolls; it updates optimistically and rolls back if the request fails.