Pressable

Touches are not delivered as raw taps. React Native negotiates a responder: when a finger goes down, every view under it is asked in turn, from the outside in, whether it wants to claim the touch, and then again from the inside out whether it will actually take it. That negotiation is what lets a button inside a scroll view work correctly - the scroll view can claim a drag that moves too far, and a nested pressable wins the touch over its container when both would otherwise claim it. This package builds press, pressIn, pressOut and longPress on top of that same negotiation, the way React Native's own Pressability does, rather than on plain touch listeners that could not express any of it.

Import
import { Pressable, TouchableOpacity } from '@ng-native/components';
Template
<pressable><touchable-opacity> once they are in the component's imports

A touch held for delayLongPress fires longPress instead of press, but only when something listens to (longPress). Without a listener there is no long press, so a slow tap is still a press, as in React Native.

On Android, a keyboard's Enter or D-pad centre and TalkBack's double-tap activate the focused control with a click rather than a touch. That fires press alone, with no pressIn or pressOut, and only on the control that has focus, as React Native's Pressability does. A pressable the click passes on its way up stops it, so a (click) on a view around a pressable does not hear a click aimed at something inside that pressable.

Pressable

<pressable> is the bare version of this: it renders a view and nothing else, but adds the press machinery. pressed is a public signal, reachable through a template reference for styling that depends on the press state without needing a class:

<pressable #p="pressable" [style.opacity]="p.pressed() ? 0.6 : 1" (press)="onTap()">
  <text>Tap me</text>
</pressable>

<touchable-opacity> is the same machinery wired to React Native's own default look: it fades to activeOpacity while pressed and eases back, using a CSS transition rather than a per-frame tween, with React Native's own asymmetric timings (150ms in, 250ms out). It eases back to the opacity its own classes or style give it, as the press ends, or to 1 when nothing sets one.

Props

Every pressable exposes disabled, hitSlop, pressRetentionOffset (how far a finger may wander before the press is canceled - separate from hitSlop, which is how far outside the view a touch may start), delayLongPress, delayPressIn, delayPressOut, minPressDuration (the pressed look is held at least this long even on the quickest tap), cancelable (whether a scrolling ancestor may take the gesture away mid-press) and, on Android, androidRipple for a native ripple.

disabled stops the presses. Whether aria-disabled and accessibilityState.disabled stop them too depends on the component, as in React Native: on a <touchable-opacity> they do when disabled is unset, as on TouchableOpacity, and on a <pressable> or a PressBehavior host they only change what VoiceOver and TalkBack announce, as on Pressable.

Nested pressables

A pressable inside a pressable - a row with a delete button in it - needs nothing extra. The negotiation elects the innermost pressable that wants the touch, so a tap on the button presses the button and never the row. What does bubble past the button is the plain touch events, (touchStart) and (touchEnd), which are not negotiated; a handler that wants an ancestor not to see one calls $event.stopPropagation().

Composing pressability onto a component

A component that wants to be pressable rather than contain something pressable - a custom button, say - composes PressBehavior through hostDirectives instead of wrapping a <pressable>. That puts the whole responder negotiation on the component's own host node at the cost of one native view rather than two, and the component calls contributeAccessibility to tell the view primitive on that same host what role and state it should announce.

A component that reacts to its own press, such as a checkbox toggling itself, forwards press in its hostDirectives entry and listens for it in host:

import { Component, signal } from '@angular/core';
import { PressBehavior } from '@ng-native/components';

@Component({
  selector: 'ui-checkbox',
  template: '<ng-content />',
  hostDirectives: [{ directive: PressBehavior, inputs: ['disabled'], outputs: ['press'] }],
  host: { '(press)': 'toggle()' },
})
export class Checkbox {
  readonly checked = signal(false);

  toggle(): void {
    this.checked.update((checked) => !checked);
  }
}

A component like that often has a reason of its own to refuse presses, a loading input for one, which its caller's disabled knows nothing of. disableWhile takes a signal, and the control is disabled while it is true or disabled is: presses are refused, a stylesheet matches :host([data-disabled]), and a screen reader is told.

import { inject, input } from '@angular/core';
import { PressBehavior } from '@ng-native/components';

// In the component:
readonly loading = input(false);

constructor() {
  inject(PressBehavior).disableWhile(this.loading);
}

The forwarding is Angular's rule rather than this package's: a host listener hears only the outputs the host exposes. Without outputs: ['press'] the (press) listener never runs, and nothing reports it. Forwarded, press also reaches a (press) the app binds on <ui-checkbox>. A component that keeps press to itself can subscribe to the behavior instead, which needs nothing forwarded: inject(PressBehavior).press.subscribe(() => this.toggle()) in the constructor.

pressabledirective@ng-native/components
import { Pressable } from '@ng-native/components';and add Pressable to the component's imports

A view that responds to presses. RN's Pressable , with the same configuration: hit slop, retention offset, press-in and press-out delays, long press, and an Android ripple.

No public members.

touchable-opacitycomponent@ng-native/components
import { TouchableOpacity } from '@ng-native/components';and add TouchableOpacity to the component's imports

A pressable that dims while pressed, as RN's TouchableOpacity does.

Inputs

activeOpacity
number = 0.2

The opacity while pressed.

Angular components as native iOS and Android views, built and shipped with Expo.

An alpha. MIT licensed. Sponsor its development.

An independent project, not affiliated with or endorsed by Google, the Angular team or Expo. Angular is a trademark of Google LLC.