Pressable
The Gesture API primitive every pressable in the library is built on.
import { Pressable } from "@delacour/native-ui/pressable";<Pressable feedback="scale" haptic="selection" onPress={open}>
<Text>Open</Text>
</Pressable>Feedback

import { PRESSABLE_FEEDBACKS, Pressable } from "@delacour/native-ui/pressable";import { Text } from "@delacour/native-ui/text";import type { ReactElement } from "react";import { View } from "react-native";export function Demo(): ReactElement { return ( <View className="gap-3"> {PRESSABLE_FEEDBACKS.map((feedback) => ( <Pressable className="rounded-xl border border-border bg-card p-4" feedback={feedback} key={feedback} testID={`feedback-${feedback}`} > <Text className="font-semibold text-base text-card-foreground">{feedback}</Text> </Pressable> ))} </View> );}
import { Pressable } from "@delacour/native-ui/pressable";import { Text } from "@delacour/native-ui/text";import type { ReactElement } from "react";import { View } from "react-native";const PRESS_STYLES = [ { label: "scale only", pressedOpacity: 1, pressedScale: 0.94 }, { label: "fade only", pressedOpacity: 0.5, pressedScale: 1 }, { label: "both", pressedOpacity: 0.7, pressedScale: 0.97 }, { label: "neither", pressedOpacity: 1, pressedScale: 1 },] as const;export function Demo(): ReactElement { return ( <View className="gap-3"> {PRESS_STYLES.map((style) => ( <Pressable className="rounded-xl border border-border bg-card p-4" key={style.label} pressedOpacity={style.pressedOpacity} pressedScale={style.pressedScale} testID={`style-${style.label}`} > <Text className="font-semibold text-card-foreground text-base">{style.label}</Text> <Text.Caption> scale {style.pressedScale}, opacity {style.pressedOpacity} </Text.Caption> </Pressable> ))} </View> );}scale, fade, scale-fade, none — the shared vocabulary, defined once. Every pressable in the
library resolves through it rather than keeping a private map: Button narrows it,
ListGroup.Item forwards it whole.
scale-fade is composed, not spelled out: its scale comes from scale and its opacity from
fade, so tuning either single-axis mode carries through.
pressedScale and pressedOpacity win on the axis they name and leave the other to feedback.
They are the escape hatch for a value the named modes do not cover — ?? is the merge, so an
explicit 0 is a value rather than an absence.
A bare Pressable with no feedback presses to { opacity: 0.9, scale: 0.97 }, which fades less
than fade does. That fallback is unnamed on purpose: naming it would either change what a bare
pressable has always done, or force scale-fade to fade less than fade.
disabled vs busy
Both block the gesture; only disabled announces the control as disabled. Use busy for a
temporary state the component clears itself, so assistive tech reports a control that is
momentarily unavailable rather than one that is inert.
Neither applies any opacity — that is the caller's variant's job.
asChild

import { Pressable } from "@delacour/native-ui/pressable";import { Text } from "@delacour/native-ui/text";import type { ReactElement } from "react";import { View } from "react-native";export function Demo(): ReactElement { return ( <Pressable asChild haptic="medium" testID="composed-card"> <View className="gap-1 rounded-xl border border-border bg-card p-4"> <Text className="font-semibold text-card-foreground text-lg">Composed card</Text> <Text.Caption>Pressable renders into this View — no extra wrapper in the tree.</Text.Caption> </View> </Pressable> );}Renders into the single child element instead of emitting a View.
Why there is no tv() here
Its values are opacity and scale interpolation targets read by a worklet on the UI thread
(1 - pressed.value * (1 - targetOpacity)), not styles. A worklet cannot compile a className, so
tv is the wrong tool — a deliberate exception, not an oversight.
Worklets must be self-contained
A worklet cannot rely on a module-scope helper. Factoring a shared clamp into a clampUnit
helper — itself marked "worklet" — crashes the UI thread with undefined is not a function: it
is not captured into the worklet's closure. No unit test sees it, because on the JS thread the
helper resolves perfectly.
Crossing back to JS

import { Pressable } from "@delacour/native-ui/pressable";import { Text } from "@delacour/native-ui/text";import type { ReactElement } from "react";import { View } from "react-native";const HAPTICS = ["selection", "light", "medium", "heavy", "success", "warning", "error"] as const;export function Demo(): ReactElement { return ( <View className="flex-row flex-wrap gap-2"> {HAPTICS.map((haptic) => ( <Pressable className="rounded-lg bg-tertiary px-3 py-2" haptic={haptic} key={haptic} testID={`haptic-${haptic}`} > <Text className="text-sm text-tertiary-foreground">{haptic}</Text> </Pressable> ))} </View> );}Use scheduleOnRN from react-native-worklets, never Reanimated's runOnJS — since Reanimated 4
that is a deprecated shim forwarding to exactly this call.
scheduleOnRN(onPress); // not runOnJS(onPress)()A Biome noRestrictedImports rule in @delacour/biome-config fails the build on the deprecated
names, so this cannot regress quietly.
Exports
import {
Pressable,
type PressableProps,
type HapticFeedback,
PRESSABLE_FEEDBACK,
PRESSABLE_FEEDBACKS,
PRESSABLE_FEEDBACK_FALLBACK,
resolvePressedState,
} from "@delacour/native-ui/pressable";API
Every pressable in the library extends this — Button, ListGroup.Item, Accordion.Trigger,
Checkbox and Radio all inherit the vocabulary rather than restating it. ViewProps passes
through, minus style.
Prop
Type
Neither disabled nor busy applies any opacity of its own — that is the caller's variant's job.
A bare Pressable with no feedback presses to { opacity: 0.9, scale: 0.97 }, which fades less
than fade does; the fallback is unnamed on purpose, so naming it could not change what a bare
pressable has always done.



