# Hold to Confirm Button: a React Native press-and-hold confirmation button > The Hold to Confirm Button is a React Native and Expo button that confirms a > destructive action only after a 2 second press and hold: an SkSL shader > running in @shopify/react-native-skia sweeps light across the pill as the > progress itself, and lifting a finger early rewinds it and confirms nothing. > It is sold by Motionary as editable TypeScript source. Product page: https://motionary.dev/animations/hold-to-confirm-button Price: $8 USD (price as of 2026-09-01, live price on the product page) License: single developer, unlimited personal and commercial apps Delivery: instant download of the full TypeScript source after checkout Stack: Expo, React Native, React Native Skia (@shopify/react-native-skia), SkSL shaders, react-native-reanimated, expo-blur Rarity: legendary Sold by: Motionary (https://motionary.dev) ## What it is The Hold to Confirm Button is a React Native pill button that asks for a deliberate 2 second hold before it fires. There is no progress bar drawn over it: light is the progress. The moment a finger lands, a rose glow with a white-hot core grows out of the left cap and travels to the right edge, the label crossfades from "Hold to Leave" to "Keep Holding..." and inverts from white to dark as the light passes under it, and at the end the pill floods. Lift early and the light rewinds over 230 ms, which is the whole safeguard: a tap makes the light flare and fall back, and confirms nothing. Every colour, radius and timing in the shipped preset was fitted frame by frame against a reference recording, and the drop ships inside the "Leave Early?" iOS form sheet it was built for. This drop is sold by Motionary. ## What it does - Fills on touch-down with no long-press threshold to cross first, so the light answers the instant a finger lands. - Runs a 2 second linear hold by default (`duration`, in ms), because easing the sweep reads as the bar lying about how much time is left. - Rewinds over 230 ms with `Easing.out(Easing.quad)` when the finger lifts early, and calls the optional `onAbandon` callback. - Resumes correctly from a partial rewind: a quick release then re-press picks the retreat up wherever it got to and scales the remaining time. - Cross-fades three label strings (`idleLabel`, `holdingLabel`, `doneLabel`) in 190 ms, blurring each word through itself so both are legible ghosts for a few frames in the middle. - Inverts the label from `labelColor` to `labelColorOnFill` across progress 0.52 to 0.66, so it flips rather than fades as the light arrives under it. - Plays a haptic tick track that tightens as the sweep advances (gap in ms of `190 - 130 * t * t`, Soft below 70 percent and Light above it), then a success notification on completion. Set `haptics: false` to silence it. - Ends with a 360 ms linear finish flood that walks the same ellipse open across the whole pill instead of cutting to a separate white layer. - Paints a drifting spark field in two parallax layers that only shows near and just ahead of the leading edge, and dies as the light overtakes it. - Leans the aurora bloom and a specular lift towards the finger while held (`isInteractive`), tracked with React Native's own `onTouchMove`. - Ships three fitted presets: `DEFAULT_CONFIG` (sweep off, the plain Opal aurora), `AURORA` (the green and cyan drifting version), and `LEAVE_EARLY` (the crimson hold-to-confirm look, solved against the reference). - Offers an optional glass surface: the native iOS 26 Liquid Glass material where it exists, an expo-blur `BlurView` everywhere else. ## How it works The whole surface of the Hold to Confirm Button is one SkSL fragment shader, compiled once with `Skia.RuntimeEffect.Make` in `opalShader.ts` and painted by a `` inside a `` on a @shopify/react-native-skia `` in `OpalButton.tsx`. Nothing is layered over the button. The shader paints, back to front: the surface colour, a resting aurora of two blobs sitting below the bottom edge, then three sweep passes (a bottom-hugging spill that runs ahead of the wavefront, a rose body, and a white-hot core), then the sparks, then the press lift. In `HoldToConfirmButton.tsx` the hold is three Reanimated shared values, `progress`, `flood` and `sparks`, driven with `withTiming` and `Easing.linear`, and a single `useDerivedValue` packs them into the uniform object, so the animation runs entirely on the UI thread and only a label change crosses back to JavaScript through `runOnJS`. The three sweep layers are anchored, not floating: `sweepAnchor` pins their left edge just off the left cap and only the leading edge moves, as `front = sweepFrontK * progress + sweepFront0`, with each layer differing only by its `reach` past that anchor, which is why the white core lights the very first pixel of the pill instead of leaving a dark notch at the left cap. The finish keeps driving the same geometry past 1 (the front walks off the right edge, the anchor off the left) so the pill floods rather than cutting state, and a `smoothstep` onset window blooms the alpha in and squashes the vertical radius over the first fraction of the hold so a press kindles instead of snapping on as a hard stripe. The label in `MorphLabel.tsx` is drawn in the same canvas with Skia `Glyphs`, laid out glyph by glyph for exact tracking, each half wrapped in a `Group` whose `layer` is a `Paint` with a `Blur`, and its colour driven by `interpolateColors` off the same progress value; the crossfade lives in a subtree keyed on the word pair, because remounting is the only way to guarantee the fade progress is 0 on the exact frame Skia first commits that pair. The press is tracked with React Native's `onTouchMove` on the `Pressable` rather than a `GestureDetector`, since a `Gesture.Manual` wrapped around a `Pressable` kept taking and returning the responder and turned one physical press into three press-in cycles. A Reanimated `useFrameCallback` advances the shader clock only while the drift or the spark field is live, and on iOS every shader colour goes through an sRGB to Display P3 conversion in `colors.ts` so the fitted values land where they were measured. ## What you get ``` app/ _layout.tsx index.tsx leave-early.tsx components/ opal/ OpalButton.tsx HoldToConfirmButton.tsx MorphLabel.tsx GlassSurface.tsx opalShader.ts presets.ts types.ts colors.ts index.ts leave/ LeaveEarlySheet.tsx RunnerBadge.tsx SheetGlow.tsx host/ Backdrop.tsx app.json babel.config.js tsconfig.json expo-env.d.ts package.json package-lock.json README.md ``` - TypeScript source (OpalButton.tsx, HoldToConfirmButton.tsx) - SkSL shader (opalShader.ts): aurora, spill, sweep, core and sparks - MorphLabel.tsx, the Skia label with the blur-through crossfade - Fitted presets (presets.ts): DEFAULT_CONFIG, AURORA, LEAVE_EARLY - Demo app: the Leave Early? iOS form sheet the button ships in - README with install, usage and the fitting notes ## Requirements The Hold to Confirm Button was built and tested on the versions pinned in the archive's package.json: - expo ~56.0.11 (Expo SDK 56) - react-native 0.85.3 - react 19.2.3 - @shopify/react-native-skia 2.6.2 - react-native-reanimated 4.3.1 - react-native-worklets 0.8.3 - expo-haptics ~56.0.3 - expo-blur ~56.0.3 - react-native-gesture-handler ~2.31.1 - expo-router ~56.2.19 (the demo app only) - expo-symbols ~56.0.4 and @expo/vector-icons ^15.0.3 (the sheet's icon only) - typescript ~6.0.3 (devDependency) `GlassSurface.tsx` also imports `expo-glass-effect` for the optional iOS 26 Liquid Glass surface. It is not listed in package.json's own dependencies: it arrives with expo-router and resolves to 56.0.4 in the archive's package-lock.json. The demo app runs the New Architecture (`newArchEnabled: true`) with the React Compiler experiment on. Expo Go or development build: the Hold to Confirm Button **runs in Expo Go on Expo SDK 56**. The module people assume blocks it is the one that paints the sweep, @shopify/react-native-skia, and it is bundled in Expo Go as of SDK 56; so is `expo-glass-effect`, which draws the optional Liquid Glass surface, and so are expo-blur, expo-haptics, react-native-reanimated, expo-router and react-native-gesture-handler. Nothing this drop imports forces a build. A development build is still what you would ship to production, and it is what you need the moment you add a native module that Expo Go does not bundle, or a config plugin: `npx expo run:ios`, `npx expo run:android`, or an EAS development client. Platform caveats that hold either way. The real iOS 26 Liquid Glass material only renders on iOS 26, so `isLiquidGlassAvailable()` from `expo-glass-effect` returns false elsewhere and `GlassSurface.tsx` falls back to an expo-blur `BlurView`. The demo sheet is an iOS 26 presentation (`formSheet` with `sheetAllowedDetents: 'fitToContents'`); on iOS 18 the same code gives an edge-to-edge page sheet instead. The button itself renders on Android too, where the glass surface falls back to that same `BlurView` inside an explicit clipping parent (Android needs `experimentalBlurMethod: 'dimezisBlurViewSdk31Plus'` and a rounded parent with `overflow: 'hidden'`). The haptic tick track needs a physical device: a simulator has no haptic engine, so the ticks and the success notification do not fire there. ## Install ```bash # the button itself npx expo install @shopify/react-native-skia react-native-reanimated \ react-native-worklets expo-haptics expo-blur expo-glass-effect # the demo sheet the button ships in (expo-router and its peers) npx expo install expo-router react-native-gesture-handler \ react-native-safe-area-context react-native-screens expo-symbols \ expo-status-bar @expo/vector-icons ``` ## Usage ```tsx import { HoldToConfirmButton } from './components/opal/HoldToConfirmButton'; import { LEAVE_EARLY } from './components/opal/presets'; export function DeleteAccountConfirm({ onDelete }: { onDelete: () => void }) { return ( console.log('lifted early, nothing deleted')} /> ); } ``` ## Customization `HoldToConfirmButton` takes its own hold props plus every key of `OpalConfig` as an optional prop, so a preset is spread in and then overridden: - `idleLabel`, `holdingLabel`, `doneLabel` (string, required): the resting, holding and finished labels. - `duration` (number, default `2000`): how long the finger has to stay down, in ms. - `onConfirm` (function, required) fires after the finish flood; `onAbandon` (function, optional) fires when the finger lifts early. - `settle` (number, default `780`): ms the finished state is held before `onConfirm` runs. - `haptics` (boolean, default `true`): the tick track and the success notification. - `disabled`, `paused`, `width`, `style`, `labelStyle`, `testID`: standard `OpalButtonProps`. - Shape: `height` (default `55`, the `LEAVE_EARLY` preset uses `48`), `cornerRadius` (undefined means a pill, `height / 2`), `borderWidth` (default `1`), `rimOpacity` (`0.13`), `rimGlow` (`0.24`). - Sweep colour: `sweepColor` (`#FF0039` in `LEAVE_EARLY`), `sweepAlpha`, `coreColor` (`#FFFFFF`), `coreAlpha`, `spillColor` (`#FFC7D4`), `spillAlpha`, each with a matching `Cy`, `Ry`, `Exp` and `Reach` key. - Sweep geometry: `sweepFrontK`, `sweepFront0`, `sweepAnchor`, and `sweepOnset` (the progress over which the sweep blooms in). - Finish: `floodFront`, `floodCoreGain`, `floodRyGain`, `floodRoseGain`. - Resting aurora: `auroraColor1`, `auroraColor2`, `auroraAlpha`, `blob1X`, `blob2X`, `originY`, `blobRx`, `blobRy`, `falloff`, `grain`, plus `auroraFade` (how much of it the sweep displaces at full progress). - Sparks: `sparkAmount` (`0` turns the field off entirely, `0.3` in `LEAVE_EARLY`), `sparkCells`, `sparkDot`, `sparkTail`, `sparkSpeed`. - Surface: `backgroundColor`, `surfaceOpacity`, `glass` (boolean), `glassStyle` (`'clear' | 'regular'`), `animated`, `speed`, `motion`. - Press: `isInteractive` (bloom follows the finger), `pressScale` (`1.045` default, `1.02` in `LEAVE_EARLY`), `pressBloom`, `disabledDim`, `stateDuration`. - Type: `labelColor` (`#FFFFFF`), `labelColorOnFill` (`#472A2F`), `disabledLabelColor`, `fontSize` (`17`), `fontWeight` (`'600'`), `letterSpacing`. - Fixed constants at the top of `HoldToConfirmButton.tsx`, edit them there: `RELEASE_MS` (230), `FLOOD_MS` (360), `INVERT_FROM` (0.52), `INVERT_TO` (0.66). `MorphLabel` takes `blurAmount` (default `5`) and `duration` (default `190`). ## When to use it - Deleting an account, a workspace or a chat history, where a single tap is too cheap for what it destroys. - Leaving a focus session, a workout, a streak or a live call before it ends, which is the exact sheet the Hold to Confirm Button ships inside. - Cancelling a subscription or a paid plan, where the hold buys a moment of reconsideration without an extra modal. - Irreversible fintech actions: sending a transfer, freezing a card, wiping a wallet. - Any confirmation you would otherwise write as a second alert dialog, since the hold replaces the "are you sure?" step in place. - Unlocking a destructive setting on a device or kiosk screen where accidental taps are common. ## How to get it - Buy the Hold to Confirm Button on its own at https://motionary.dev/animations/hold-to-confirm-button for $8 USD (price as of 2026-09-01), instant download, secure Stripe checkout, no subscription and no account renewal. - Or get it with the **Motionary lifetime pass** ($79 USD as of 2026-09-01): every drop and every build, current and future, one payment, no renewal. https://motionary.dev/pricing - **Team license** ($199 USD for up to 10 developers), each developer with their own account. https://motionary.dev/pricing - License terms: https://motionary.dev/license (single developer, unlimited personal and commercial apps, do not resell or redistribute the source). - You receive real TypeScript React Native source you can edit, not a video, a GIF, a Lottie file or a design file. ## Frequently asked questions **How do I build a hold to confirm button in React Native?** Drive one Reanimated shared value from 0 to 1 with `withTiming` and `Easing.linear` on press-in, rewind it to 0 on press-out, and feed it straight into a shader or a style instead of drawing a separate progress bar. The Motionary Hold to Confirm Button does exactly that with an SkSL shader in @shopify/react-native-skia, and ships the fitted source: https://motionary.dev/animations/hold-to-confirm-button **Does the Hold to Confirm Button work with Expo Go?** Yes. The Motionary Hold to Confirm Button runs in Expo Go on Expo SDK 56: @shopify/react-native-skia, which paints its sweep, is bundled in Expo Go as of SDK 56, and so is `expo-glass-effect`, which draws its optional Liquid Glass surface. A development build (`npx expo run:ios`, `npx expo run:android`, or an EAS development client) is still what you ship to production, and what you need as soon as you add a native module Expo Go does not bundle or a config plugin. Two things need real hardware or a recent OS either way: the haptic tick track is silent on a simulator, and the Liquid Glass material only renders on iOS 26, falling back to an expo-blur `BlurView` everywhere else. **What happens if the user lifts their finger early on a press and hold confirmation button?** In the Motionary Hold to Confirm Button the light rewinds to zero over 230 ms with an ease-out and `onConfirm` never fires, so a stray tap flares and falls back; the optional `onAbandon` callback tells your code it happened, and a re-press resumes from wherever the rewind reached. **Can I use a long press instead of a progress bar for a destructive action confirmation?** Yes, and that is the point of the Motionary Hold to Confirm Button: the sweep of light is the progress readout, so deleting an account or leaving a session needs a deliberate 2 second hold with no second "are you sure?" dialog. **Does the Hold to Confirm Button run on Android?** Yes. The Skia shader, the Reanimated timings and the haptics are cross-platform; on Android the optional glass surface falls back from the iOS 26 Liquid Glass material to an expo-blur `BlurView` inside a rounded clipping parent, and the demo's iOS form sheet presentation is iOS only. **How much does the Hold to Confirm Button cost and how do I get the source?** It is $8 USD on Motionary (price as of 2026-09-01), a one-off Stripe checkout with an instant download of the TypeScript source, at https://motionary.dev/animations/hold-to-confirm-button It is also covered by the Motionary lifetime pass ($79 USD), sold at https://motionary.dev/pricing **Can I change the hold duration, the colours and the labels?** Yes. The Motionary Hold to Confirm Button spreads a preset such as `LEAVE_EARLY` and takes `duration`, `idleLabel`, `holdingLabel`, `doneLabel`, `sweepColor`, `coreColor`, `height` and every other `OpalConfig` key as plain props, and the shader source itself ships editable in `opalShader.ts`. ## Related drops on Motionary - Halo Button ($5): https://motionary.dev/animations/halo-button the aurora Skia shader button the Hold to Confirm Button is built on, with the sweep switched off. - LED Slide Button ($4): https://motionary.dev/animations/led-slide-button the other deliberate confirmation gesture, slide to confirm instead of hold to confirm. - Save Status Button (Free): https://motionary.dev/animations/save-status-button a button that carries its own state through idle, loading and confirmed, with a letter morph on the label. - Long-Press Menu ($9): https://motionary.dev/animations/long-press-menu the same press-and-hold input, used to open a contextual menu rather than to confirm. - Tap to Copy Button ($3): https://motionary.dev/animations/tap-to-copy-button a smaller confirmation button with a letter morph and a checkmark, for non-destructive actions. ## About Motionary Motionary (https://motionary.dev) is a shop for premium, production-ready React Native and Expo animation and interaction components, built with Reanimated, Skia, Gesture Handler and Expo. Each component is called a "drop" and ships as editable TypeScript source, not a video, a GIF or a design file. Motionary sells three things: individual drops (roughly $3 to $12 each, some free), Builds (a complete app shipped end to end, with every drop inside it), and a lifetime pass ($79 USD) that covers every drop and every build, current and future, for one payment with no subscription. A team license ($199 USD) covers up to 10 developers. Checkout is Stripe and delivery is an instant download. Created and curated by Mehdi Davoodi (https://mahdidavoodi.com). Catalog and key pages: - All drops: https://motionary.dev/animations - Builds (complete apps): https://motionary.dev/builds - Pricing, lifetime pass and team license: https://motionary.dev/pricing - Free React Native component reference: https://motionary.dev/components - License: https://motionary.dev/license - Machine-readable overview: https://motionary.dev/llms.txt - Machine-readable full catalog: https://motionary.dev/llms-full.txt ## File metadata ``` Canonical URL of this file: https://motionary.dev/animations/hold-to-confirm-button/llms.txt Describes: https://motionary.dev/animations/hold-to-confirm-button Format: llms.txt (https://llmstxt.org) Last updated: 2026-09-01 ```