StealThis .dev

Expo Haptic Feedback

Haptic feedback patterns using expo-haptics — impact styles (light, medium, heavy), notification types (success, warning, error), and selection feedback.

react-native typescript expo-haptics
Targets: React Native

Expo Snack

Code

import React, { useRef, useState } from "react";
import { Animated, Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import * as Haptics from "expo-haptics";

interface HapticButton {
  label: string;
  description: string;
  color: string;
  flashColor: string;
  onPress: () => void;
}

function HapticCard({ button }: { button: HapticButton }) {
  const scale = useRef(new Animated.Value(1)).current;
  const [flashing, setFlashing] = useState(false);

  const handlePress = () => {
    button.onPress();
    setFlashing(true);

    Animated.sequence([
      Animated.timing(scale, {
        toValue: 0.92,
        duration: 80,
        useNativeDriver: true,
      }),
      Animated.timing(scale, {
        toValue: 1,
        duration: 120,
        useNativeDriver: true,
      }),
    ]).start();

    setTimeout(() => setFlashing(false), 200);
  };

  return (
    <Pressable onPress={handlePress}>
      <Animated.View
        style={[
          styles.card,
          {
            backgroundColor: flashing ? button.flashColor : "#1e293b",
            transform: [{ scale }],
          },
        ]}
      >
        <View style={[styles.cardIndicator, { backgroundColor: button.color }]} />
        <View style={{ flex: 1 }}>
          <Text style={styles.cardLabel}>{button.label}</Text>
          <Text style={styles.cardDesc}>{button.description}</Text>
        </View>
      </Animated.View>
    </Pressable>
  );
}

export default function App() {
  const impactButtons: HapticButton[] = [
    {
      label: "Light",
      description: "Subtle tap — good for small UI elements",
      color: "#38bdf8",
      flashColor: "#0c4a6e",
      onPress: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light),
    },
    {
      label: "Medium",
      description: "Standard tap — buttons and toggles",
      color: "#6366f1",
      flashColor: "#312e81",
      onPress: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium),
    },
    {
      label: "Heavy",
      description: "Strong tap — confirmations and emphasis",
      color: "#a78bfa",
      flashColor: "#4c1d95",
      onPress: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy),
    },
  ];

  const notificationButtons: HapticButton[] = [
    {
      label: "Success",
      description: "Positive outcome — task completed",
      color: "#34d399",
      flashColor: "#064e3b",
      onPress: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success),
    },
    {
      label: "Warning",
      description: "Caution — requires attention",
      color: "#fbbf24",
      flashColor: "#78350f",
      onPress: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning),
    },
    {
      label: "Error",
      description: "Failure — something went wrong",
      color: "#f87171",
      flashColor: "#7f1d1d",
      onPress: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error),
    },
  ];

  const selectionButtons: HapticButton[] = [
    {
      label: "Selection",
      description: "Micro-tap — pickers, sliders, segment controls",
      color: "#e2e8f0",
      flashColor: "#334155",
      onPress: () => Haptics.selectionAsync(),
    },
  ];

  return (
    <ScrollView style={styles.container} contentContainerStyle={styles.content}>
      <Text style={styles.heading}>Haptic Feedback</Text>
      <Text style={styles.subtitle}>Tap each button to feel the haptic pattern</Text>

      <Text style={styles.sectionTitle}>Impact</Text>
      {impactButtons.map((btn) => (
        <HapticCard key={btn.label} button={btn} />
      ))}

      <Text style={styles.sectionTitle}>Notification</Text>
      {notificationButtons.map((btn) => (
        <HapticCard key={btn.label} button={btn} />
      ))}

      <Text style={styles.sectionTitle}>Selection</Text>
      {selectionButtons.map((btn) => (
        <HapticCard key={btn.label} button={btn} />
      ))}
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0f172a",
  },
  content: {
    paddingTop: 60,
    paddingHorizontal: 20,
    paddingBottom: 40,
  },
  heading: {
    fontSize: 28,
    fontWeight: "700",
    color: "#f8fafc",
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 14,
    color: "#94a3b8",
    marginBottom: 32,
  },
  sectionTitle: {
    fontSize: 13,
    fontWeight: "600",
    color: "#64748b",
    textTransform: "uppercase",
    letterSpacing: 1.2,
    marginBottom: 10,
    marginTop: 8,
  },
  card: {
    flexDirection: "row",
    alignItems: "center",
    borderRadius: 12,
    padding: 16,
    marginBottom: 10,
    gap: 14,
  },
  cardIndicator: {
    width: 6,
    height: 36,
    borderRadius: 3,
  },
  cardLabel: {
    fontSize: 16,
    fontWeight: "600",
    color: "#f1f5f9",
    marginBottom: 2,
  },
  cardDesc: {
    fontSize: 13,
    color: "#94a3b8",
  },
});

Expo Haptic Feedback

A demo showcasing every haptic feedback pattern available in expo-haptics. Buttons are grouped by category — impact, notification, and selection — with visual feedback on press so you can see and feel each haptic type.

Features

  • Impact styles — Light, Medium, and Heavy impact feedback via Haptics.impactAsync.
  • Notification types — Success, Warning, and Error notification haptics via Haptics.notificationAsync.
  • Selection feedback — Subtle selection tap via Haptics.selectionAsync, ideal for toggles and pickers.
  • Visual feedback — Each button animates with a scale press and color flash to confirm the haptic was fired.

Usage

Install the dependency and use the component:

npx expo install expo-haptics

Each button triggers a different haptic pattern. The visual feedback (color flash and scale animation) helps confirm which pattern was activated, even when testing on a simulator without haptic hardware.