Icon button

Free · MITReact Native · Expo

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.

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/icon-button.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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.
number99
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.
booleanfalse
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.