AniUI Academy

Pressable and Touch Handling

The modern touch-response primitive in React Native: onPress and onLongPress, style-as-a-function press feedback, and hitSlop for bigger touch targets.

8 min read

A phone has no mouse. There's no hover state to sniff before a click, no right-click context menu waiting in the wings — touch is the entire input model, and React Native's job is to turn "a finger touched this region of the screen" into an event your component can react to.

Where Pressable fits

Early React Native shipped a family of components for this: TouchableOpacity (dims on press), TouchableHighlight (adds a highlight underlay), TouchableWithoutFeedback (no built-in visual feedback at all), and later TouchableNativeFeedback (Android-only ripple). You will still run into all of these in real, shipping codebases — they aren't broken, and migrating a large app off them isn't always worth doing on its own. But for new code, Pressable is the current recommended default. Instead of a different component per feedback style, it's one component with a flexible API that covers all of those cases and more.

import { Pressable, Text } from "react-native";
 
function LikeButton({ onLike }: { onLike: () => void }) {
  return (
    <Pressable onPress={onLike}>
      <Text>Like</Text>
    </Pressable>
  );
}

onPress and onLongPress

onPress fires on a normal tap-and-release within the element's bounds. onLongPress fires when the user presses and holds past a threshold — useful for a distinct "hold" action that shouldn't be triggered by an ordinary tap.

<Pressable
  onPress={() => console.log("opened")}
  onLongPress={() => console.log("show context menu")}
>
  <Text>Message bubble</Text>
</Pressable>

These are two different gestures with two different intents. A single onPress handler that tries to guess "was that a long press?" by measuring time itself is exactly the kind of thing onLongPress exists so you never have to write.

Press feedback: style as a function

This is the part of Pressable that genuinely replaces the old Touchable family. Instead of a plain style object, style can be a function that receives the current interaction state and returns the style to use:

<Pressable
  onPress={onLike}
  style={({ pressed }) => ({
    opacity: pressed ? 0.5 : 1,
    backgroundColor: pressed ? "#eee" : "#fff",
    padding: 12,
    borderRadius: 8,
  })}
>
  <Text>Like</Text>
</Pressable>

pressed is true for exactly as long as a finger is down on the element. There's no useState to wire up and no re-render to reason about by hand — Pressable re-invokes the style function itself as the press state changes. This one pattern reproduces TouchableOpacity's dimming, TouchableHighlight's underlay, or any custom feedback you want, all from the same component.

style can also be a plain object if you don't need press-state-dependent styling at all — the function form is only worth reaching for when the style actually depends on pressed (or the other states the callback receives, like hovered on platforms that support it).

hitSlop: a bigger touch target without a bigger element

Design and usability guidance for touch targets (roughly 44 points on iOS, 44dp on Android, both long-standing platform recommendations) doesn't always line up with how big an icon is supposed to look. A 16px trash-can icon is easy to see and easy to mis-tap.

hitSlop solves this by extending the touchable area without touching the visual one:

<Pressable
  onPress={onDelete}
  hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
>
  <TrashIcon size={16} />
</Pressable>

The icon still renders at 16px. But a touch landing up to 12 logical pixels outside its edges in any direction still counts as a press on it. This matters because it's easy to conflate "make it easier to tap" with "make it bigger" — hitSlop is proof those are separable problems, and solving the first doesn't require compromising a design that calls for a small icon.

hitSlop also accepts a single number as shorthand for equal slop on all four sides: hitSlop={12}.

Putting it together

A realistic pressable list row combines several of these:

function ListRow({ label, onPress, onLongPress }: RowProps) {
  return (
    <Pressable
      onPress={onPress}
      onLongPress={onLongPress}
      hitSlop={8}
      style={({ pressed }) => [
        styles.row,
        pressed && styles.rowPressed,
      ]}
    >
      <Text style={styles.label}>{label}</Text>
    </Pressable>
  );
}

Notice style here returns an array — React Native accepts arrays of style objects (later entries win on conflicting properties), which is the standard way to layer a conditional style on top of a base one, whether or not you're inside the function form.

What to remember

  • Pressable is the current recommended default for touch handling; the older TouchableOpacity/TouchableHighlight/TouchableWithoutFeedback family still appears in real code but has been superseded.
  • onPress handles a normal tap; onLongPress handles a distinct press-and-hold gesture — don't fake one by timing the other.
  • style={({ pressed }) => ...} computes feedback styling from the live press state, with no extra local state needed.
  • hitSlop enlarges the touchable area without changing the visual size — a small icon can still be easy to hit accurately.
  • Touch target sizing and visual sizing are separate concerns; Pressable's API reflects that separation directly.

Check yourself

4 questions · pass 3/4 to unlock TextInput and Forms

up to 50
  1. 1.Given the Touchable* family (TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback) still shows up in older codebases, what's the current recommended default for handling touch in new React Native code?

  2. 2.What does the function-as-style pattern style={({ pressed }) => ...} let you do that a static style object can't?

  3. 3.What problem does hitSlop solve?

  4. 4.Why might a component use both onPress and onLongPress?

4 left to answer