# Haptic Button

> A React haptic button that feels native: spring press, touch ripple, a haptic tick, a long-press action and a hold-to-confirm mode.

- Type: React component (Mobile)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add haptic-button`
- Docs: https://wingo-ui.com/components/haptic-button

## Interaction

Tap it and it dips on a quick spring while a ripple spreads from under your finger; phones play a light tick (touch and pen only, a mouse click never vibrates). With onLongPress, holding a finger or pen for 450ms plays a medium tick and runs the secondary action; a right click, Shift+F10 or the ContextMenu key do the same, and the click after a long press is swallowed. In hold mode a click does nothing: hold the finger, the mouse button, Space or Enter and the button fills from the start edge; let go early and the fill springs back, a quick tap shakes the button, plays a warning tick and shows a "Hold to confirm" chip above it. Moving more than 8px, leaving the button, switching tabs or blurring the window cancels. When the fill completes the button pops, onConfirm runs (a promise shows the spinner), then a check replaces the label for 1.2s with a success tick; a rejected promise shakes with an error tick. Screen readers, voice control and switch access, which activate with a plain click and cannot hold, get a two-step path: the first activation shows and announces "Activate again to confirm", a second one within 4s confirms. Outcomes are announced to screen readers. Under reduced motion there is no dip, ripple, pop or shake; the fill still grows.

## Installation

```bash
npx wingo-ui@latest add haptic-button
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/haptic-button` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/haptic-button.tsx`

npm dependencies: `npm i lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))

## Usage

```tsx
import { Trash2 } from "lucide-react"
import { HapticButton } from "@/components/ui/haptic-button"

export function DeleteClient({ onDelete }: { onDelete: () => Promise<void> }) {
  return (
    <HapticButton
      tone="danger"
      variant="soft"
      mode="hold"
      holdDuration={1200}
      leftIcon={<Trash2 />}
      fullWidth
      onConfirm={onDelete}
      labels={{
        holdHint: "Hold to delete",
        confirmed: "Deleted",
        activateAgain: "Activate again to delete",
      }}
    >
      Delete Old Mill Bakery
    </HapticButton>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` | `"Send"` | The label. For icon sizes pass the icon and an aria-label. The playground swaps the sample for "Hold to send" in hold mode. |
| `variant?` | `"solid" \| "soft" \| "outline" \| "ghost"` | `"solid"` | The button's variants (no link: a link has no surface to ripple or fill). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Semantic color from the design tokens. danger for a hold to delete, brand for a hold to pay. |
| `color?` | `string` | `""` | Any CSS color. Replaces the tone in every variant; text on a solid fill turns black or white by lightness. |
| `size?` | `"sm" \| "md" \| "lg" \| "icon-sm" \| "icon" \| "icon-lg"` | `"md"` | The button's sizes: 32 / 40 / 48px, square icon sizes. On touch screens md grows to 44px and sm gets a 44px hit area. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape, scaled per size like the button. The ripple and the fill follow it. |
| `effect?` | `"scale" \| "ripple" \| "both" \| "none"` | `"both"` | Press feedback: the 0.97 dip on a snappy spring, a ripple spreading from the touch point (keyboard presses ripple from the center), both, or none. In hold mode the fill replaces the ripple. |
| `rippleColor?` | `string` | `""` | The ripple color. Empty uses the text color at 16%; a custom color shows at 30%. |
| `loading?` | `boolean` | `false` | Spinner in place of the label, width kept, aria-busy; presses and holds are ignored and focus stays. Also on while an onConfirm promise runs. |
| `loadingText?` | `ReactNode` |  | Text next to the spinner while loading. The button reserves room for it. |
| `disabled?` | `boolean` | `false` | Dims the button and blocks presses, holds and long presses. |
| `fullWidth?` | `boolean` | `false` | Stretches to the container width, the usual choice for the main action on a phone. |
| `mode?` | `"press" \| "hold"` | `"press"` | press is a normal button. hold: a click does nothing; holding a finger, the mouse button, Space or Enter for holdDuration fills the button and calls onConfirm. For destructive or money actions that deserve more than a tap but less than a dialog. |
| `holdDuration?` | `number` | `1200` | Milliseconds of holding to confirm, clamped to 300..5000. |
| `showSuccess?` | `boolean` | `true` | After onConfirm (and its promise) a check replaces the label for 1.2s and the button gets data-confirmed. |
| `haptics?` | `boolean` | `true` | Haptic ticks through lib/haptics on touch and pen presses (never for a mouse or on desktops). Turn off app-wide with configureHaptics({ enabled: false }). |
| `hapticKind?` | `"selection" \| "light" \| "medium" \| "heavy" \| "success" \| "warning" \| "error"` | `"light"` | The press-mode tick. Hold mode always plays light at the start, success on confirm, warning on a too-short tap and error on a rejected promise; a long press plays medium. |
| `type?` | `"button" \| "submit" \| "reset"` | `"button"` | Defaults to "button". In hold mode a submit button never submits on a click; submit from onConfirm. |
| `leftIcon?` | `ReactNode` |  | An icon before the label. Unsized svgs follow the size (14 / 16 / 18px). |
| `rightIcon?` | `ReactNode` |  | An icon after the label. |
| `spinner?` | `ReactNode` |  | Replaces the default spinner. |
| `onClick?` | `(event: MouseEvent<HTMLButtonElement>) => void` |  | Press mode only, and not while loading or disabled. Hold mode never calls it: put the action in onConfirm. |
| `onConfirm?` | `() => (void \| Promise<unknown>)` |  | Hold mode's action, called when the fill completes. A promise shows the loading state, then the check; a rejection shakes the button, plays an error tick and announces labels.failed. |
| `onHoldStart?` | `() => void` |  | A hold began (finger, pen, mouse button, Space or Enter). |
| `onHoldCancel?` | `() => void` |  | A hold ended before the fill completed: released, moved past 8px, left the button, window blurred, or disabled. |
| `onLongPress?` | `(event: LongPressEvent<HTMLButtonElement>) => void` |  | Press mode's secondary action: a 450ms touch or pen hold (through useLongPress), a right click, Shift+F10 or the ContextMenu key. The click after a long press is swallowed; screen readers hear labels.longPressHint. |
| `labels?` | `Partial<HapticButtonLabels>` |  | Strings: holdHint "Hold to confirm", holding "Keep holding", confirmed "Done", failed "That did not work, try again", longPressHint "Press and hold for more options", activateAgain "Activate again to confirm" (hold mode, after a click from assistive tech). DEFAULT_HAPTIC_BUTTON_LABELS holds the defaults. |
| `classNames?` | `Partial<Record<"effects" \| "ripple" \| "fill" \| "content" \| "icon" \| "spinner" \| "check" \| "hint", string>>` |  | Extra classes for the inner parts: the effects layer, each ripple, the hold fill, the label, icons, spinner, check and hint chip. |
| `className?` | `string` |  | Extra classes for the button itself, merged last. Override the tone vars here, e.g. [--tone-foreground:white]. |

## Parts

### DEFAULT_HAPTIC_BUTTON_LABELS (constant)

The English strings: holdHint ("Hold to confirm"), holding ("Keep holding"), confirmed ("Done"), failed, longPressHint ("Press and hold for more options") and activateAgain, which assistive tech users see in hold mode. Spread it into labels to translate a few. Type: HapticButtonLabels.

## Examples

- **Effects**: scale, ripple, both or none; ghost and soft buttons ripple over a photo too.
- **Hold to delete**: mode="hold" with the danger tone: the row fades out only after a full hold.
- **Hold to pay**: An async onConfirm shows the spinner, then the check. holdDuration 1500ms.
- **Long press for more**: onLongPress opens an action sheet; right click and Shift+F10 work on desktop.
- **Icon buttons**: Toggles with aria-pressed; the heart pops on the bouncy spring.
- **Sizes**: sm, md and lg in solid and outline, the button's own scale.
- **Custom color**: color="#0F766E" with a light rippleColor.

## See also

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Slide To Confirm](https://wingo-ui.com/components/slide-to-confirm) ([Markdown](https://wingo-ui.com/components/slide-to-confirm.md))
- [Delete Button](https://wingo-ui.com/components/delete-button) ([Markdown](https://wingo-ui.com/components/delete-button.md))
- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))
- [Action Sheet](https://wingo-ui.com/components/action-sheet) ([Markdown](https://wingo-ui.com/components/action-sheet.md))

---

Canonical: https://wingo-ui.com/components/haptic-button
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
