Haptic patterns

햅틱 패턴

A sequence of vibration pulses with varied length and spacing — feedback you can feel with sound off.

Also known as: Vibration patternsTaptic feedback
···
html
<div class="wrap">
  <div class="rows">
    <div class="row"><span class="lbl">tap</span><div class="seq" id="s-tap"><i class="on" style="--w:1"></i></div></div>
    <div class="row"><span class="lbl">success</span><div class="seq" id="s-ok"><i class="on" style="--w:1"></i><i class="off" style="--w:.6"></i><i class="on" style="--w:1"></i></div></div>
    <div class="row"><span class="lbl">warning</span><div class="seq" id="s-warn"><i class="on" style="--w:3"></i></div></div>
  </div>
  <button class="play" id="play" type="button" aria-label="play sound"><span class="tri"></span></button>
</div>
css
.wrap{position:relative;width:92%;height:84%;display:flex;flex-direction:column;justify-content:center;gap:14%}
.row{display:flex;align-items:center;gap:8px}
.lbl{width:22%;font:600 10px/1 monospace;color:var(--muted);text-align:right}
.seq{flex:1;display:flex;gap:3px;height:16px}
.seq i{height:100%;border-radius:3px;flex-grow:var(--w);background:var(--line)}
.seq i.on{background:var(--accent-3)}
.row.go .seq i.on{background:var(--accent)}
.row.go{animation:shake .3s}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-2px)}75%{transform:translateX(2px)}}
.play{position:absolute;right:0;bottom:0;width:clamp(26px,14%,38px);aspect-ratio:1;border-radius:50%;border:1px solid var(--line);background:var(--surface);display:grid;place-items:center;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.12)}
@media (min-width:460px){
  .lbl{font-size:14px}
  .seq{height:26px}
  .seq i{border-radius:5px}
}
.play .tri{width:0;height:0;border-style:solid;border-width:6px 0 6px 9px;border-color:transparent transparent transparent var(--fg);margin-left:2px}
.play:active{transform:scale(.92)}
js
const rows = ['s-tap', 's-ok', 's-warn'].map((id) => document.getElementById(id).closest('.row'));
let idx = 0;
setInterval(() => {
  rows.forEach((r) => r.classList.remove('go'));
  rows[idx].classList.add('go');
  idx = (idx + 1) % rows.length;
}, 1000);
function thump(ctx, t, dur) {
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = 'sine';
  osc.frequency.value = 90;
  gain.gain.setValueAtTime(0, t);
  gain.gain.linearRampToValueAtTime(0.14, t + 0.01);
  gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
  osc.connect(gain).connect(ctx.destination);
  osc.start(t);
  osc.stop(t + dur + 0.02);
}
document.getElementById('play').addEventListener('click', () => {
  if (navigator.vibrate) { try { navigator.vibrate([60, 90, 60]); } catch (e) {} }
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  const now = ctx.currentTime;
  thump(ctx, now, 0.08);
  thump(ctx, now + 0.15, 0.08);
  setTimeout(() => ctx.close(), 500);
});

A phone's linear actuator (like iOS's Taptic Engine) doesn't just switch on and off — it shapes vibration waveforms ranging from a short sharp "tick" to a long soft "hum." A pattern is these pulses laid out along a timeline, expressible on the web as a single array via navigator.vibrate([vibrateMs, pauseMs, vibrateMs, ...]).

Meaning is usually carried by pulse length and count — one short pulse reads as "light acknowledgment" (tap feedback), two quick pulses as "done/success," one long pulse as "warning/error." Vibration is more private than sound (nobody nearby hears it) and survives silent mode, but it's useless if the device isn't in hand and drains battery, so always design it alongside a sound or visual signal. Overusing the same vibration (every keystroke) numbs the fingertip, so save it for moments that actually matter.

Used for keyboard-tap haptics, message/notification arrival, payment confirmation, and hit/fire feedback in game controllers.

When to use

Reach for it at moments that must survive silent mode. Overusing the same pattern numbs it — spend it sparingly.