{"id":181,"date":"2026-03-20T11:03:04","date_gmt":"2026-03-20T15:03:04","guid":{"rendered":"https:\/\/kimsal.com\/blog\/?p=181"},"modified":"2026-03-20T11:03:04","modified_gmt":"2026-03-20T15:03:04","slug":"a-bit-of-feedback","status":"publish","type":"post","link":"https:\/\/kimsal.com\/blog\/2026\/03\/20\/a-bit-of-feedback\/","title":{"rendered":"A bit of feedback&#8230;"},"content":{"rendered":"\n<p>A small bit of feedback&#8230; that&#8217;s often what a mobile user is looking for.   Haptic feedback &#8211; a quick device vibration &#8211; is great.  It&#8217;s subtle, quick, doesn&#8217;t interrupt, but gives an actual *feeling* that something happened.  And&#8230; on iOS, it&#8217;s harder to do without building a full &#8216;native mobile app&#8217;.   iOS Safari doesn&#8217;t support the &#8216;vibrate&#8217; method.  Apple does do some things quite well, but holding off on browser support for this has just been&#8230; annoying.<br><br>Some time ago Apple introduced a &#8216;toggle switch&#8217; attribute on the &#8216;checkbox&#8217; input type.  And clicking it&#8230; provides small haptic feedback.  And this morning I found a small <a href=\"https:\/\/github.com\/posaune0423\/use-haptic\">React hook<\/a> that will trigger this for you.<\/p>\n\n\n\n<p>I created a similar version for Vue 3<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { ref, computed, onMounted, onUnmounted } from 'vue'\n\nconst HAPTIC_DURATION = 10\n\nfunction detectIOS(): boolean {\n  return \/iPad|iPhone|iPod\/.test(navigator.userAgent) &amp;&amp; !(window as any).MSStream\n}\n\nexport function useHaptic(duration = HAPTIC_DURATION) {\n  const inputRef = ref&lt;HTMLInputElement | null>(null)\n  const labelRef = ref&lt;HTMLLabelElement | null>(null)\n  const isIOS = computed(() => detectIOS())\n\n  onMounted(() => {\n    const input = document.createElement('input')\n    input.type = 'checkbox'\n    input.id = 'haptic-switch'\n    input.setAttribute('switch', '')\n    input.style.display = 'none'\n    document.body.appendChild(input)\n    inputRef.value = input\n\n    const label = document.createElement('label')\n    label.htmlFor = 'haptic-switch'\n    label.style.display = 'none'\n    document.body.appendChild(label)\n    labelRef.value = label\n  })\n\n  onUnmounted(() => {\n    if (inputRef.value) document.body.removeChild(inputRef.value)\n    if (labelRef.value) document.body.removeChild(labelRef.value)\n  })\n\n  function triggerHaptic() {\n    if (!isIOS.value &amp;&amp; navigator?.vibrate) {\n      navigator.vibrate(duration)\n    } else {\n      labelRef.value?.click()\n    }\n  }\n\n  return { triggerHaptic }\n}<\/code><\/pre>\n\n\n\n<p>Which can then be used as so&#8230;<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script setup lang=\"ts\">\nimport { useHaptic } from '@\/composables\/useHaptic';\n\nconst { triggerHaptic } = useHaptic();\n....\n\nfunction handleClick(type: string) {\n    triggerHaptic();\n\/\/ other code here\n}\n...<\/code><\/pre>\n\n\n\n<p>Simple, and it works just fine.<\/p>\n\n\n\n<p>Maybe in 2027 we&#8217;ll get the ability to provide haptic feedback in regular web apps natively without hacks?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A small bit of feedback&#8230; that&#8217;s often what a mobile user is looking for. Haptic feedback &#8211; a quick device vibration &#8211; is great. It&#8217;s subtle, quick, doesn&#8217;t interrupt, but gives an actual *feeling* that something happened. And&#8230; on iOS, it&#8217;s harder to do without building a full &#8216;native mobile app&#8217;. iOS Safari doesn&#8217;t support&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"categories":[1],"tags":[],"class_list":["post-181","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/posts\/181","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/comments?post=181"}],"version-history":[{"count":1,"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/posts\/181\/revisions"}],"predecessor-version":[{"id":182,"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/posts\/181\/revisions\/182"}],"wp:attachment":[{"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/media?parent=181"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/categories?post=181"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kimsal.com\/blog\/wp-json\/wp\/v2\/tags?post=181"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}