VueJs Search Input With SpeechRecognition API

Adam Bailey • February 25, 2024

vue

Share:

I 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:

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 input
41 this.searchMethod();
42 } else {
43 // else just give us the plain ol' paginated list
44 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:

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 state
2recognition.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 active
23<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!

You might like other posts in
vue