Tappable
Pre-alphaThe registry and the CLI are not published yet.Roadmap

Tappable

Headless press primitive: pressed state, disabled state and a minimum touch target.

Edit profile
NameAda Lovelace
Username@ada
Press an element → pressed false · onPress 0

Press the close icon or a button. Each one changes its background while pressed, and slide your finger off before releasing to cancel the press. Turn on Touch areas: the icon is 20pt but its pressable area is 44pt.

Installation

npx axiom add tappable
pnpm dlx axiom add tappable
yarn dlx axiom add tappable
bun x axiom add tappable

What it does

Tappable is the press layer under Button and IconButton. It wraps React Native’s Pressable and handles what every pressable element needs:

  • it tells the component it wraps whether it is pressed;
  • it blocks presses when disabled;
  • it extends the touch area of small elements to touchTarget;
  • it sets the accessibility role and state.

Tappable draws nothing. The pressed color comes from the component that renders it, through its component tokens: button.solid.pressed.background, button.outline.pressed.background… That’s why the solid and outline buttons above react differently while the press behavior is the same.

You rarely render it yourself. Use it directly when you build your own pressable component, like a row, a tile or a card.

Usage

import { Tappable } from "@/components/core/tappable";

function Tile({ title, onPress, disabled }) {
	const { components } = useTheme();

	return (
		<Tappable
			onPress={onPress}
			disabled={disabled}
			accessibilityLabel={title}
		>
			{({ pressed }) => (
				<View
					style={{
						backgroundColor: pressed
							? components.button.outline.pressed.background
							: components.button.outline.default.background,
					}}
				>
					<Text color={disabled ? "disabled" : "default"}>{title}</Text>
				</View>
			)}
		</Tappable>
	);
}

children can be a function that receives { pressed }, or a plain element when the look doesn’t change on press. style accepts the same kind of function:

<Tappable
	onPress={onPress}
	style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
>
	<Text>Settings</Text>
</Tappable>

Pressed state

StepWhat happens
Finger downonPressIn fires and pressed becomes true.
Finger up on the elementpressed becomes false, onPressOut then onPress fire.
Finger slides off the elementpressed becomes false and onPressOut fires. onPress doesn’t.
Held for delayLongPress msonLongPress fires instead of onPress.

A press also cancels when the parent ScrollView starts scrolling, so scrolling a list doesn’t press its rows.

Touch target

1010
Icon 24×24
hitSlop 10
6
Small chip 64×32
hitSlop { x: 0, y: 6 }
Button 96×44
hitSlop 0

A blue outline is the area that accepts the press. Tappable measures the element and adds hitSlop on each axis where it is smaller than touchTarget (44pt):

hitSlop = Math.max(0, (touchTarget - size) / 2);

A 24pt icon gets 10 on each side. A 64×32 chip gets 6 above and below and nothing on the sides. A 44pt tall button gets nothing.

hitSlop doesn’t change the layout, so a small icon keeps its size and position. Keep enough space around it: when two touch areas overlap, the element rendered last gets the press.

Pass hitSlop yourself to override the computed value, or minTouchTarget={false} to turn the behavior off, for example in a dense grid where the layout already gives each cell 44pt.

Disabled

Turn on Disabled in the first preview: nothing reacts, and the colors come from the disabled component tokens.

With disabled:

  • onPress, onPressIn, onPressOut and onLongPress don’t fire;
  • pressed stays false;
  • screen readers announce the element as dimmed or disabled.

A disabled element still takes up its touch area. A press on it does nothing and doesn’t reach the element behind.

Props

PropTypeDefaultDescription
onPress(event: GestureResponderEvent) => voidCalled when a press ends on the element.
onPressIn(event: GestureResponderEvent) => voidCalled when the finger touches the element.
onPressOut(event: GestureResponderEvent) => voidCalled when the finger lifts or leaves the element.
onLongPress(event: GestureResponderEvent) => voidCalled after delayLongPress ms. Replaces onPress for that press.
delayLongPressnumber500Duration in ms before onLongPress.
disabledbooleanfalseBlocks every press callback and the pressed state.
hitSlopnumber | InsetscomputedExtra touch area around the element. Overrides the computed value.
minTouchTargetbooleantrueComputes hitSlop from touchTarget when hitSlop isn’t set.
childrenReactNode | ({ pressed }) => ReactNodeContent, or a function of the pressed state.
styleStyleProp<ViewStyle> | ({ pressed }) => StyleProp<ViewStyle>Style, or a function of the pressed state.
accessibilityRoleAccessibilityRole'button'Role announced by screen readers.
accessibilityLabelstringLabel announced by screen readers. Required when the content has no text.

Other Pressable props are passed through.

Accessibility

  • The role is button by default. Set link, tab or another role when the element is not a button.
  • accessibilityState.disabled follows disabled, so VoiceOver and TalkBack announce it.
  • An element with only an icon has nothing to read. Give it an accessibilityLabel. IconButton requires one.
  • The computed hitSlop helps people with motor impairments too, not only people with large fingers.

Used by