An open, portable format for haptics — and the first built for AI agents.
Almost nobody ships haptics on the web, and barely anyone in apps — the APIs are fragmented, the good ones are buried in native code, and the feel is hard to author and impossible to port. Meanwhile AI agents scaffold entire UIs all day and never add a single line of touch feedback, because there is nothing for them to reach for.
haptics.md is an open, portable haptic format + registry. One human-readable descriptor — a .hap.md file — compiles to the best available feedback on iOS, Android, and the web, degrades honestly when the hardware can't deliver, and is legible enough that an agent can author, place, and compile it without guessing.
haptics.md is a developer tool, but its reason to exist is accessibility. The north star is haptic braille — giving any phone with a vibration motor a standardized, hardware-free way to convey a braille cell, a digit, a wayfinding cue, or a silent confirmation, and documenting how developers implement it consistently so every app stops inventing its own incompatible buzz.
Held honestly, because accessibility punishes hype:
- Complement, never replacement. Blind and low-vision people already use phones fluently with screen readers and refreshable braille displays. This adds one hardware-free channel; it does not "enable" anyone.
- Slow, by physics. One motor means roughly one braille cell per second — good for letters, digits, short codes, confirmations, and wayfinding cues, not for reading paragraphs. Any demo implying otherwise is wrong.
- Web-limited. On the web it is Android-Chrome-only and a silent no-op on iPhone Safari and desktop. Rich braille needs a native app.
- A draft proposal, not a shipped feature. Nothing here has been co-designed with blind braille readers yet. Per our governance, no default, timing, or claim is "recommended" until blind braille readers and the relevant braille authorities have shaped it — a careful, multi-year track (see the roadmap). We publish the starting point in the open precisely to invite that collaboration.
If you read braille, work in accessibility, or represent a braille authority or a blind-led organization, we want you shaping this — start with docs/haptic-braille.md and spec/braille.md.
The fastest way to understand this is to feel it.
→ haptics.md — open on your phone.
Android (Chrome family) plays the duration-only web fallback — best-effort, no intensity or sharpness. The full track only renders in a native app. iPhone Safari shows an explicit "haptics unavailable" state — iOS Safari has no vibration API at all, and we will not fake it with a hacky buzz. All demo media leads on Android for the same reason.
npx haptics add pull-to-refresh --platform iosThis copies a pattern into your project — you own the file, edit it freely, there is no runtime dependency to install. The CLI vendors the registry into its own package, so add works offline and keeps working if the site is down. (It is not "no backend" — it is a snapshot shipped with the tool.)
Available platforms: ios, android, web, react-native, flutter.
A .hap.md is a Markdown file with three parts: YAML frontmatter, human prose describing the feel, and exactly one fenced code block tagged haptic containing the JSON that is the compilable source of truth.
---
id: pull-to-refresh
title: Pull to refresh
spec: "0.1"
version: 1.0.0
category: gesture
intent: impact.light
requiredCapability: null
platforms: [ios, android, web:best-effort]
tags: [scroll, refresh, threshold]
feel: A light tick the instant the refresh threshold is crossed.
---
# Pull to refresh
Fires once, at the moment the user pulls past the refresh threshold — a
single light tick that says "released, we've got it." Not a confirmation
of success; that comes later, when the content actually lands.
```haptic
{
"haptic": "0.1",
"id": "pull-to-refresh",
"intent": "impact.light",
"track": [
{ "at": 0, "type": "transient", "intensity": 0.6, "sharpness": 0.7 }
],
"fallbacks": {
"web": [10],
"reduced": "impact.light"
}
}
```The same descriptor compiles to:
iOS — UIFeedbackGenerator / iOS 17 .sensoryFeedback, resolving the intent to a native semantic primitive first (best feel):
.sensoryFeedback(.impact(weight: .light), trigger: didCrossThreshold)Android — VibrationEffect.Composition primitives, with the VIBRATE manifest permission and an honest capability guard that falls back rather than lying:
// AndroidManifest.xml: <uses-permission android:name="android.permission.VIBRATE" />
val vib = vibratorManager.defaultVibrator // VibratorManager: API 31+
if (vib.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) {
vib.vibrate(
VibrationEffect.startComposition()
.addPrimitive(PRIMITIVE_LOW_TICK, 0.6f) // scale from track intensity
.compose()
)
} else {
// No composition primitives (common on mid-range ERM devices):
// drop to a duration-only effect. Never pretend amplitude worked.
vib.vibrate(VibrationEffect.createOneShot(10, DEFAULT_AMPLITUDE))
}Web — navigator.vibrate, duration-only and best-effort:
// web (Android Chrome), best-effort, duration-only.
// No intensity, no sharpness — a gesture-gated on/off buzz, and a
// no-op on iOS Safari and desktop. This is the crudest rung, by design.
navigator.vibrate?.([10]);Every target resolves top-down, deterministically, and stops at the first rung it can deliver:
intent— a native semantic primitive (best feel).track— the synthesized timeline of transient/continuous events.fallbacks.web— crude duration-only on/off milliseconds.- silent no-op — a valid outcome.
A device with no actuator is a valid target that produces nothing. App logic must never gate on haptic success, and a runtime must never report that intensity or sharpness "worked" when it was flattened away.
| Platform | Tier | Note |
|---|---|---|
| iOS | First-class | UIFeedbackGenerator, iOS 17 .sensoryFeedback, Core Haptics + AHAP. Full intent + track. |
| Android | First-class | VibrationEffect + Composition primitives. VibratorManager is API 31+; amplitude control is API 26+ and many devices report hasAmplitudeControl() = false; mid-range ERM devices often fail areAllPrimitivesSupported(). Guard and fall back. |
| Web | First-class, best-effort | navigator.vibrate: Android Chrome family only, duration-only, gesture-gated, throttled. Does not exist on iOS Safari or desktop. Intensity and sharpness never survive to this target. |
| React Native | Intent-tier only | expo-haptics exposes semantic constants only. Custom track/sharpness needs a native module that has not shipped. |
| Flutter | Intent-tier only | HapticFeedback exposes semantic constants only. Same limitation as React Native. |
| Game controllers | Rumble-only escape hatch | Gamepad rumble is weak/strong motors, no waveform. An escape hatch, not a target for authored feel. |
The full in-scope / out-of-scope matrix lives in SPEC.md.
These nine tokens are stable and safe to build on:
impact.light · impact.medium · impact.heavy · impact.soft · impact.rigid · notification.success · notification.warning · notification.error · selection
Everything else in the spec is haptic/0.1-draft and explicitly unstable. RFC-2119 MUST/SHOULD language applies only to this vocabulary and to the degradation contract above. Unknown future fields must be ignored, never treated as fatal.
Haptics are a feedback channel — including for deaf and hard-of-hearing users — delivered with consent. Runtimes respect the OS system-haptics setting (iOS: Settings > Accessibility > Touch > Vibration), not prefers-reduced-motion. Vibration and motion are different channels with different user intent behind them.
Agents generate most new UI and add no touch feedback — they build the screens and skip the feel. haptics.md gives them something to reach for.
Now:
- Six authored Skills covering pattern selection,
.hap.mdauthoring, compilation, capability-guarding, honest degradation, and haptic braille. - An
llms.txtthat describes the format, the frozen vocabulary, and the degradation contract in a form an agent can load directly.
Later:
- A compiler-backed MCP server so an agent can author a
.hap.md, compile it to a target, and verify the capability guards in one loop.
- Copy an existing
.hap.mdfrom the registry. - Rewrite the frontmatter, the prose, and the one
hapticJSON block for your interaction. - Keep to the frozen intent vocabulary where you can; add a
trackfor the feel; always include afallbacks.weband a sanereducedintent. - Open a PR with a DCO sign-off (
git commit -s).
Patterns are built only from public, documented platform APIs. See CONTRIBUTING.md for the full checklist.
- ROADMAP.md — where this is going and what is frozen vs. provisional.
- SPEC.md — the
haptic/0.1-draftgrammar, the intent vocabulary, and the scope matrix. - CONTRIBUTING.md — pattern checklist, DCO, and review bar.
- docs/haptic-braille.md — the haptic-braille developer guide (the north star).
- spec/braille.md — the
braille/0.1-draftcell-to-haptic encoding.
The site is Astro, deployed on Cloudflare Pages; the registry ships as static JSON so the CLI works offline and from the CDN edge. Local dev: npm install, then npm run dev.
Code — Apache-2.0 © Bimbi-Digital. Pattern snippets — CC0 (public domain), copy and own them freely.
A note on patents: Apache-2.0's patent grant covers only patents held by contributors on their own contributions. It does not provide freedom-to-operate against third-party haptic patents (e.g. Immersion). These patterns are built only from public, documented platform APIs, with no clearance guarantee — evaluate your own exposure before shipping.