VueJs Search Input With SpeechRecognition API
Adam Bailey • February 25, 2024
vueI recently needed to incorporate Speech Recognition to an existing search input on a VueJs application. This post outlines how I did that, and gives you a reasonably good starting point to do the same.
What is the SpeechRecognition API?
SpeechRecognition is a Web Speech API interface that allows you to convert speech to text using Javascript in the browser.
This Post picks up from a search input I had already built using VueJs and Tailwind in Laravel JetStream application using IntertiaJS. The article describing how to build this search input can be found here.
The Search Input Component
Here's the entire component where we left off in the previous article:
1<template> 2 <div class="w-full bg-white px-4 flex"> 3 <label for="search" class="hidden">Search</label> 4 <input 5 id="search" 6 ref="search" 7 v-model="search" 8 class="transition h-10 w-full bg-gray-100 border border-gray-500 rounded-full focus:border-purple-400 outline-none cursor-pointer text-gray-700 px-4 pb-0 pt-px" 9 :class="{ 'transition-border': search }"10 autocomplete="off"11 name="search"12 placeholder="Search"13 type="search"14 @keyup.esc="search = null"15 @blur="search = null"16 />17 </div>18</template>19 20<script>21 import { defineComponent } from "vue";22 import { usePage } from "@inertiajs/inertia-vue3";23 24 export default defineComponent({25 props: {26 // any route name from laravel routes (ideally index route is what you'd search through)27 routeName: String,28 },29 30 data() {31 return {32 // page.props.search will come from the backend after search has returned.33 search: usePage().props.value?.search || null,34 };35 },36 37 watch: {38 search() {39 if (this.search) {40 // if you type something in the search input41 this.searchMethod();42 } else {43 // else just give us the plain ol' paginated list44 this.$inertia.get(route(this.routeName));45 }46 },47 },48 49 methods: {50 searchMethod: _.debounce(function () {51 this.$inertia.get(52 route(this.routeName),53 { search: this.search },54 { preserveState: true }55 );56 }, 500),57 },58 });59</script>
We have a:
- Slightly styled input field.
- Single prop of
routeName
which accepts the route name from the Laravel route above, such as'stories.index'
. - Data property that looks in the inertia page props for a search value.
- Watcher on that
search
data property. - Method that uses lodash
debounce
to only fetch results every 500 milliseconds.
Adding Speech Recognition
let's add a button to enable the listener for the SpeechRecognition API. Just below the input field, add a button:
1<button @click="startVoiceRecognition">2 Click to start voice recognition!3</button>
And in the script
section, we add the startVoiceRecognition
method we called in the template:
1startVoiceRecognition() { 2 const recognition = new (window.SpeechRecognition || 3 window.webkitSpeechRecognition)(); 4 recognition.interimResults = true; 5 6 recognition.addEventListener("result", (event) => { 7 let transcript = Array.from(event.results) 8 .map((result) => result[0]) 9 .map((result) => result.transcript)10 .join("");11 12 if (event.results[0].isFinal) {13 this.search = transcript;14 }15 });16 17 recognition.start();18},
This method creates a new instance of the SpeechRecognition
object, and sets the
interimResults
property to true
.
When event.results[0].isFinal
is true
, it sets the search
data property to the transcript
value from the results
event.
This alone should be enough to get the speech recognition working in your search input, but we can do a little more to improve the user experience.
Add start and end event listeners
Since we don't yet have anything telling the user the input is listening, we can add a listening
data property to use
for toggling styles on the button.
1data() {2 return {3 // page.props.search will come from the backend after search has returned.4 search: usePage().props.value?.search || null,+ listening: false, 6 };7},
We will add event listeners for the start
and end
events of the SpeechRecognition API. Add this right before the
recogniotion.start()
method:
1// keep the voice active state in sync with the recognition state2recognition.addEventListener("start", () => {3 this.listening = true;4});5 6recognition.addEventListener("end", () => {7 this.listening = false;8});
Now we can use the listening
data property to toggle the input and button's styles:
1<input 2 id="search" 3 ref="search" 4 v-model="search" 5 class="h-8 w-full cursor-pointer rounded-full border border-blue-700 bg-gray-100 px-4 pb-0 pt-px text-gray-700 outline-none transition focus:border-blue-400" + :class="{ 'border-red-500 border-2': listening }" 7 autocomplete="off" 8 name="search" 9 :placeholder="searchPlaceholder"10 type="search"11 @keyup.esc="search = null"12/>13<button @click="startVoiceRecognition" + :class="{ + 'text-red-500': listening, + 'listening': !listening, + }" 18>19 Click to start voice recognition!20</button>21 22// this maintains the styles of the button when it's active23<style scoped>24 .listening:active {25 @apply text-red-500;26 }27</style>
Please add your own styles as you see fit. I'm using Tailwind.css classes here. In my application, I also used an SVG inside the button to apply the styles to. This article should just give a basic outline of how to add the styles.
I also have used InertiaJs in my application, so you may have to adjust the searchMethod
method to fit how your application.
Here is the entire component with the SpeechRecognition API added:
1<template> 2 <div class="w-full px-2 bg-transparent flex"> 3 <label for="search" class="hidden">Search</label> 4 <input 5 id="search" 6 ref="search" 7 v-model="search" 8 class="h-8 w-full cursor-pointer rounded-full border border-blue-700 bg-gray-100 px-4 pb-0 pt-px text-gray-700 outline-none transition focus:border-blue-400" 9 :class="{ 'border-red-500 border-2': listening }" 10 autocomplete="off" 11 name="search" 12 :placeholder="searchPlaceholder" 13 type="search" 14 @keyup.esc="search = null" 15 /> 16 <button 17 :class="{ 18 'text-red-500': listening, 19 'listening': !listening, 20 }" 21 @click="startVoiceRecognition" 22 > 23 Click to start voice recognition! 24 </button> 25 </div> 26</template> 27 28<script> 29import { defineComponent } from "vue"; 30import { usePage } from "@inertiajs/inertia-vue3"; 31 32export default defineComponent({ 33 props: { 34 routeName: { 35 type: String, 36 required: true, 37 }, 38 label: { 39 type: String, 40 default: null, 41 }, 42 }, 43 44 data() { 45 return { 46 search: usePage().props.value?.search || null, 47 listening: false, 48 }; 49 }, 50 51 computed: { 52 typeName() { 53 return this.label || this.routeName.split(".")[0] || "something"; 54 }, 55 searchPlaceholder() { 56 return this.listening 57 ? "Listening..." 58 : `Search ${this.typeName}!`; 59 }, 60 }, 61 62 watch: { 63 search() { 64 if (this.search) { 65 this.searchMethod(); 66 } else { 67 this.$inertia.get(route(this.routeName)); 68 } 69 }, 70 }, 71 72 methods: { 73 searchMethod: _.debounce(function () { 74 this.$inertia.get( 75 route(this.routeName), 76 { search: this.search }, 77 { preserveState: true } 78 ); 79 }, 500), 80 81 startVoiceRecognition() { 82 const recognition = new (window.SpeechRecognition || 83 window.webkitSpeechRecognition)(); 84 recognition.interimResults = true; 85 86 recognition.addEventListener("result", (event) => { 87 let transcript = Array.from(event.results) 88 .map((result) => result[0]) 89 .map((result) => result.transcript) 90 .join(""); 91 92 if (event.results[0].isFinal) { 93 this.search = transcript; 94 } 95 }); 96 97 // keep the voice active state in sync with the recognition state 98 recognition.addEventListener("start", () => { 99 this.listening = true;100 });101 102 recognition.addEventListener("end", () => {103 this.listening = false;104 });105 106 recognition.start();107 },108 },109});110</script>111 112<style scoped>113.listening:active {114 @apply text-red-500;115}116</style>
Conclusion
At this point you should have a working search input in your page which listens for speech when the button is clicked, then automatically updates the search input with the spoken words, when speech has been recognized.
I have certainly been enjoying how fast I can code out my ideas using VueJs and Tailwind as a starting point for my applications. I can reuse this component in any of my applications and it will work just fine. I hope you find this useful too.
Happy coding!