ProgressButton

A button held, not tapped, to confirm — a fill grows from the leading edge and the action fires when it reaches the end.

Open Source

Installation

ProgressButton ships with the library — install it once, then import:

import { ProgressButton } from "@delacour/react-native-ui/progress-button";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add progress-button

Usage

app/index.tsx
import { ProgressButton } from "@/components/ui/progress-button";
import { View } from "react-native";

export default function Example() {
  return (
    <View className="flex-1 items-center justify-center p-4">
      <ProgressButton isAutoReset onComplete={() => console.log("erased")} variant="destructive">
        <ProgressButton.Label>Hold to erase</ProgressButton.Label>
      </ProgressButton>
    </View>
  );
}

Anatomy

<ProgressButton onComplete={erase} variant="destructive">
  <Icon icon={IconTrashCan} />
  <ProgressButton.Label>Hold to erase</ProgressButton.Label>
  <ProgressButton.Done />
</ProgressButton>

For an irreversible action that would otherwise need a confirmation dialog — erasing, paying. Two taps in a row are a rhythm a hand falls into; a two-second hold cannot be done by accident or by habit. Nothing fires before the fill completes, and there is no tolerance near the end. Let go early and the fill plays back at the same rate; press again and it resumes from where it is.

Hold to erase

ProgressButton.Done is optional. Without one the button draws a tick in the fill's foreground.

Variants

Every variant rests on the same secondary surface and carries its colour in the label and the fill. The label is drawn twice — once on the surface, once inside the fill — so it stays readable exactly where the wipe crosses it.

Variants
<ProgressButton variant="primary">Hold to confirm</ProgressButton>
<ProgressButton variant="secondary">Hold to archive</ProgressButton>
<ProgressButton variant="destructive">Hold to delete</ProgressButton>
<ProgressButton variant="success">Hold to approve</ProgressButton>
VariantResting labelFillLabel inside the fill
primaryprimaryprimaryprimary-foreground
secondarysecondary-foregroundforegroundbackground
destructivedestructivedestructivedestructive-foreground
successsuccesssuccesssuccess-foreground

Sizes

The Button's own heights, padding and label steps, so a progress button sits level with a Button beside it. There is no square icon size: a hold has to say what it does.

Sizes
Sizes
<ProgressButton size="sm">Small</ProgressButton>
<ProgressButton size="md">Medium</ProgressButton>
<ProgressButton size="lg">Large</ProgressButton>

Shape

pill, the default, takes the button's capsule corner, so the wipe's leading edge goes out as a curve. rounded takes the card's corner, rounded-lg, to sit square with a rounded Button.

Rounded
Rounded
<View className="flex-row gap-3">
  <Button className="flex-1 rounded-lg" variant="outline">
    Cancel
  </Button>
  <ProgressButton className="flex-1" shape="rounded" variant="success">
    Hold to send
  </ProgressButton>
</View>

Hold duration

holdDuration is the time a full hold takes, in milliseconds — 2000 by default, never less than 200.

<ProgressButton holdDuration={1200}>Hold to check in</ProgressButton>

Resetting

Once complete the button is locked: a release does not drain it and further presses are ignored. isAutoReset rewinds it after autoResetDelay (1000 by default). A rewind travels back; it never snaps to empty.

<ProgressButton autoResetDelay={1500} isAutoReset>
  Hold to check in
</ProgressButton>

Controlled

isCompleted with onCompletedChange puts the state in your hands. Accept the hold in the same update — set isCompleted to true — or the fill travels back, read as declined. For async work, accept at once, show the work in a custom Done, and set isCompleted back to false to rewind on failure.

const [phase, setPhase] = useState<"idle" | "sending" | "sent">("idle");

<ProgressButton isCompleted={phase !== "idle"} onComplete={submit}>
  <ProgressButton.Label>Hold to submit</ProgressButton.Label>
  <ProgressButton.Done>
    {phase === "sending" ? <Spinner /> : null}
    <ProgressButton.Label>{phase === "sent" ? "Sent" : "Sending"}</ProgressButton.Label>
  </ProgressButton.Done>
</ProgressButton>

Custom done

A ProgressButton.Done you write replaces the default tick. A bare Icon and a ProgressButton.Label inside it take the fill's foreground.

Custom done
<ProgressButton isAutoReset size="lg" variant="success">
  <Icon icon={IconCreditCard1} />
  <ProgressButton.Label>Hold to pay</ProgressButton.Label>
  <ProgressButton.Done>
    <Icon icon={IconLock} />
    <ProgressButton.Label>Paid</ProgressButton.Label>
  </ProgressButton.Done>
</ProgressButton>

Disabled

isDisabled fades the button and turns the gesture off, so the fill never starts.

<ProgressButton isDisabled variant="destructive">
  Hold to erase
</ProgressButton>

Haptics

Off by default. haptic plays when the hold takes, and a success knock follows on completion.

<ProgressButton haptic="light" onComplete={erase} variant="destructive">
  Hold to erase
</ProgressButton>

Reduced motion

With Reduce Motion on, the fill advances in five steps instead of sweeping. It stays an indicator, because a control that asks you to wait and shows nothing is broken. The hold still takes its full duration.

Accessibility

The root is a button whose hint, by default, says it has to be held, with accessibilityState={{ disabled, checked }} — checked once complete. A screen reader cannot hold, so the button's activate action completes it outright: the hint has already announced what it does, and refusing would leave the action unreachable. The label inside the fill and the done mark are hidden from assistive technology, so nothing is read twice.

API Reference

ProgressButton

Extends React Native's ViewProps.

Prop

Type

ProgressButton.Label

The button's text, coloured for the layer it is drawn in. Extends React Native's TextProps.

Prop

Type

ProgressButton.Done

Shown over the full fill on completion. Extends ViewProps.

Prop

Type

useProgressButton

Returns { variant, size, isCompleted, progress } — progress a SharedValue<number> from 0 to 1 — for a custom child that follows the hold. Throws outside a <ProgressButton>.

On this page