suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
  • AI
GitHubCreate
DocsComponentsPatternsInteractionsChartsBlocksAICreate
Components84
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Code Block
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Trace
  • Wheel Picker
  • Widget

suiss/ui · Component

Haptics

A provider that connects components to your haptics library, with a hook to trigger feedback.

Haptics is an open-source component from suiss/ui for React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

iOS
Haptics on iOS in light modeHaptics on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { Stepper, StepperDecrement, StepperGroup, StepperIncrement, StepperInput, StepperSeparator } from "@/components/interactions/stepper"
import { Checkbox } from "@/components/ui/checkbox"
import { Chip } from "@/components/ui/chip"
import { type HapticsAdapter, HapticsProvider } from "@/components/ui/haptics"
import { Slider } from "@/components/ui/slider"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"

function useLoggingAdapter() {
  const [log, setLog] = React.useState({ last: "None", count: 0 })
  const adapter = React.useMemo<HapticsAdapter>(() => {
    const record = (event: string) =>
      setLog((current) => ({ last: event, count: current.count + 1 }))
    return {
import EsuissUI
import SwiftUI

struct HapticsRow<Content: View>: View {
  let label: LocalizedStringKey
  @ViewBuilder let content: () -> Content

  var body: some View {
    HStack(spacing: 16) {
      Text(label)
        .textStyle(.base)
        .foregroundStyle(EsuissColor.foreground)
      Spacer(minLength: 0)
      content()
    }
    .frame(minHeight: 44)
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember

Components such as Amount Input, Chart and Context Menu play haptic feedback through useHaptics(). By default nothing happens: HapticsProvider lets you plug in an adapter that calls the haptics library of your choice, so the components stay free of a native dependency.

The adapter has three methods: impact(style) with light, medium, heavy, soft or rigid; selection(); and notification(type) with success, warning or error.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/haptics

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add haptics
$ pnpm dlx @esuiss/ui@latest add haptics --platform swift
$ pnpm dlx @esuiss/ui@latest add haptics --platform kotlin

Manual

  1. Add the pieces it builds on: the primitives components/primitives/haptics.tsx.

  2. Copy the source into components/ui/haptics.tsx:

uniwind/components/ui/haptics.tsx
import * as React from "react"
import {
  type HapticsAdapter,
  HapticsContext,
  type HapticsContextValue,
  type HapticsImpactStyle,
  type HapticsNotificationType,
  NOOP_HAPTICS_ADAPTER,
  useHaptics,
} from "@/components/primitives/haptics"

function HapticsProvider({
  adapter,
  enabled = true,
  children,
}: {
  1. Update the import paths to match your project.
  1. Add the pieces it builds on: the primitives components/primitives/haptics.tsx.

  2. Copy the source into components/ui/haptics.tsx:

nativewind/components/ui/haptics.tsx
import * as React from "react"
import {
  type HapticsAdapter,
  HapticsContext,
  type HapticsContextValue,
  type HapticsImpactStyle,
  type HapticsNotificationType,
  NOOP_HAPTICS_ADAPTER,
  useHaptics,
} from "@/components/primitives/haptics"

function HapticsProvider({
  adapter,
  enabled = true,
  children,
}: {
  1. Update the import paths to match your project.
  1. Add the pieces it builds on: the primitives components/primitives/haptics.tsx.

  2. Copy the source into components/ui/haptics.tsx:

stylesheet/components/ui/haptics.tsx
import * as React from "react"
import {
  type HapticsAdapter,
  HapticsContext,
  type HapticsContextValue,
  type HapticsImpactStyle,
  type HapticsNotificationType,
  NOOP_HAPTICS_ADAPTER,
  useHaptics,
} from "@/components/primitives/haptics"

function HapticsProvider({
  adapter,
  enabled = true,
  children,
}: {
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Copy the theme files every view uses:

swift/Theme/Theme.swift
import CoreText
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source:
swift/Components/Haptics.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var hapticsEnabled = true
}

extension View {
  public func haptics(_ enabled: Bool) -> some View {
    environment(\.hapticsEnabled, enabled)
  }

  func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
    modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
  }

  func hapticFeedback<Trigger: Equatable>(
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import android.content.res.AssetManager
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
  1. Copy the source:
kotlin/components/Haptics.kt
package com.esuiss.ui.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

val LocalHapticsEnabled = staticCompositionLocalOf { true }

@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { HapticsProvider } from "@/components/ui/haptics"
<ControlsDemo />

Wrap your app in HapticsProvider and pass an adapter object that implements impact, selection and notification. Set enabled={false} to turn all feedback off, for example from a user setting.

Call useHaptics() in your own components to get the same methods. Without a provider, or when disabled, they do nothing.

Examples

Feedback

Buttons for each impact, selection and notification type.

iOS
Haptics (feedback) on iOS in light modeHaptics (feedback) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { type HapticsAdapter, HapticsProvider, useHaptics } from "@/components/ui/haptics"
import { Text } from "@/components/ui/text"

function useLoggingAdapter() {
  const [log, setLog] = React.useState({ last: "None", count: 0 })
  const adapter = React.useMemo<HapticsAdapter>(() => {
    const record = (event: string) =>
      setLog((current) => ({ last: event, count: current.count + 1 }))
    return {
      impact: (style) => record(`impact ${style}`),
      selection: () => record("selection"),
      notification: (type) => record(`notification ${type}`),
    }
import EsuissUI
import SwiftUI

struct HapticsLog: View {
  let last: String
  let count: Int

  var body: some View {
    HStack {
      Text("Last haptic")
        .textStyle(.sm)
        .foregroundStyle(EsuissColor.mutedForeground)
      Spacer(minLength: 16)
      Text(verbatim: count > 0 ? "\(last) (\(count))" : last)
        .textStyle(.sm, weight: .medium)
        .foregroundStyle(EsuissColor.foreground)

Reorder

Feedback while dragging items in a reorder list.

iOS
Haptics (reorder) on iOS in light modeHaptics (reorder) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { ReorderList, ReorderListHandle, ReorderListItem } from "@/components/interactions/reorder-list"
import { type HapticsAdapter, HapticsProvider } from "@/components/ui/haptics"
import { Text } from "@/components/ui/text"

function useLoggingAdapter() {
  const [log, setLog] = React.useState({ last: "None", count: 0 })
  const adapter = React.useMemo<HapticsAdapter>(() => {
    const record = (event: string) =>
      setLog((current) => ({ last: event, count: current.count + 1 }))
    return {
      impact: (style) => record(`impact ${style}`),
      selection: () => record("selection"),
      notification: (type) => record(`notification ${type}`),
    }
import EsuissUI
import SwiftUI

struct HapticsReorderDemo: View {
  @State private var tasks = ["Inbox", "Today", "Upcoming", "Someday"]

  var body: some View {
    ReorderList($tasks) { task in
      Text(verbatim: task)
        .textStyle(.base)
        .foregroundStyle(EsuissColor.foreground)
        .lineLimit(1)
        .padding(.vertical, 12)
    }
    .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
    .overlay {
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue

Disabled

The same controls with haptics turned off.

iOS
Haptics (disabled) on iOS in light modeHaptics (disabled) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { Stepper, StepperDecrement, StepperGroup, StepperIncrement, StepperInput, StepperSeparator } from "@/components/interactions/stepper"
import { Checkbox } from "@/components/ui/checkbox"
import { Chip } from "@/components/ui/chip"
import { type HapticsAdapter, HapticsProvider } from "@/components/ui/haptics"
import { Slider } from "@/components/ui/slider"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"

function useLoggingAdapter() {
  const [log, setLog] = React.useState({ last: "None", count: 0 })
  const adapter = React.useMemo<HapticsAdapter>(() => {
    const record = (event: string) =>
      setLog((current) => ({ last: event, count: current.count + 1 }))
    return {
import EsuissUI
import SwiftUI

struct HapticsDisabledExample: View {
  var body: some View {
    HapticsControls()
      .haptics(false)
  }
}

struct HapticsRow<Content: View>: View {
  let label: LocalizedStringKey
  @ViewBuilder let content: () -> Content

  var body: some View {
    HStack(spacing: 16) {
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember

API Reference

01

HapticsProvider

Supplies the adapter to every component below it.

PropTypeDefaultDescription
adapterHapticsAdapter | null-Object with impact, selection and notification methods.
enabledbooleantrueTurns feedback on or off. Defaults to true.
childrenReact.ReactNode-

Notes

  • The types HapticsAdapter, HapticsImpactStyle and HapticsNotificationType are exported for typing your adapter.
  • Haptics have no visual output; pair important feedback with a visible change.
NextHeaderPreviousFullscreen Menu

On this page

  1. 01Installation
    • Manual
  2. 02Usage
  3. 03Examples
    • Feedback
    • Reorder
    • Disabled
  4. 04API Reference
    • HapticsProvider
  5. 05Notes