Informing a screen reader in Vue.js through the use of aria-live features

My goal is to make a vue component that dynamically announces information to a screen reader when certain events occur on my website.

I have managed to achieve this functionality by having a button populate a span with text containing the attributes aria-live="assertive" and role="alert". This works well initially, but when I click on other buttons with similar behavior, NVDA reads the previous text twice before reading the new text. This issue seems to be specific to vue, as a similar setup using jquery does not have the same problem. I suspect that it has something to do with how vue renders to the DOM.

I am hoping to find a workaround for this issue or discover a better way to present the text to users without encountering this problem. Any assistance would be greatly appreciated.

I have created a simple component in a working code sandbox to demonstrate the issue I am facing (navigate to components/HelloWorld.vue for the code) -- Please note: The content of this sandbox may have changed based on the answer provided below. Below is the full code for the component:

export default {
  name: "HelloWorld",
  data() {
    return {
      ariaText: ""
    };
  },
  methods: {
    button1() {
      this.ariaText = "This is a bunch of cool text to read to screen readers.";
    },
    button2() {
      this.ariaText = "This is more cool text to read to screen readers.";
    },
    button3() {
      this.ariaText = "This text is not cool.";
    }
  }
};
<template>
  <div>
    <button @click="button1">1</button>
    <button @click="button2">2</button>
    <button @click="button3">3</button><br/>
    <span role="alert" aria-live="assertive">{{ariaText}}</span>
  </div>
</template>

Answer №1

After conducting some experimentation, I have discovered a more reliable approach. Instead of simply replacing the text within an element with new content, I recommend adding a new element to a parent container containing the updated text. My method involves storing the text in an array of strings that can be looped through using v-for and displayed within an aria-live container.

To assist those interested in implementing this technique, I have developed a comprehensive component that demonstrates different ways to achieve the desired outcome:

export default {
    props: {
        value: String,
        ariaLive: {
            type: String,
            default: "assertive",
            validator: value => {
                return ['assertive', 'polite', 'off'].indexOf(value) !== -1;
            }
        }
    },
    data() {
        return {
            textToRead: []
        }
    },
    methods: {
        say(text) {
            if(text) {
                this.textToRead.push(text);
            }
        }
    },
    mounted() {
        this.say(this.value);
    },
    watch: {
        value(val) {
            this.say(val);
        }
    }
}
.assistive-text {
    position: absolute;
    margin: -1px;
    border: 0;
    padding: 0;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
}
<template>
    <div class="assistive-text" :aria-live="ariaLive" aria-relevant="additions">
        <slot></slot>
        <div v-for="(text, index) in textToRead" :key="index">{{text}}</div>
    </div>
</template>

This solution can be implemented by setting a variable on the parent as the v-model of the component. Any changes to that variable will be read aloud by a screen reader once and also whenever the parent container is tab-focused.

The functionality can also be triggered programmatically using

this.$refs.component.say(textToSay);
— please note that this trigger will occur again when the parent container receives focus. To prevent this behavior, consider placing the element within a non-focusable container.

Furthermore, the component features a slot for inserting text dynamically like so:

<assistive-text>Text to speak</assistive-text>
. However, ensure that the text is not bound to a dynamic/mustache variable to avoid issues when the text undergoes changes.

I have provided an updated version of the sandbox referenced in the original question, which showcases a functional example of this component.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Steps to trigger a Bootstrap modal when the user clicks anywhere on the webpage

I need assistance with setting up a Bootstrap dialogue modal to open at the clicked position on a mousedown event when a user interacts with the page. Despite my attempts, the dialogue modal is not opening where it should be. Here's what I've tri ...

The most effective method for acquiring an object through inheritance

I'm seeking advice on the best practice for adding behavior to an object received as a JSON object. I have REST services that allow me to define a sort of state machine. The API defines a /sessions resources. When creating a session via POST /sessio ...

Steps for updating object state in React

Here is the code snippet that I am working with: this.state = { user: null } I am trying to figure out how to set the name property of the user when it exists. Unfortunately, using this syntax this.setState({user.name: 'Bob') doesn't ...

I'm puzzled by the inconsistency of my scrolltop function on mobile, as it seems to be scrolling to various parts of the page depending on my

I am encountering an issue with a function that scrolls to a specific element when a button is clicked. Strangely, this functionality works fine on desktop but fails on mobile devices. It successfully scrolls to the correct position only when at the top of ...

Don't let noise linger in the background unnoticed

Within my HTML table, there are 32 cells that each possess an onclick="music()" function. This function is currently functioning correctly, with one small exception. I desire for the functionality to be such that whenever I click on a different cell, the m ...

What is the correct way to apply styles universally instead of using "*" as a selector?

With Nextron, I was able to successfully run my code, but upon opening the window, I noticed that the body tag had a margin of 8px. Although I managed to change this using the dev tools, I am unsure how to permanently apply this change in my code. When att ...

Guide on integrating next-images with rewrite in next.config.js

I'm currently facing a dilemma with my next.config.js file. I've successfully added a proxy to my requests using rewrite, but now I want to incorporate next-images to load svg files as well. However, I'm unsure of how to combine both functio ...

Pass multiple variables from a Laravel controller to a Vue component

Let's consider a Vue component as shown below: export default { created() { axios.get("a url").then(res => { console.log(res.data); }); } }; Next, Axios sends a request to the following function in a Laravel co ...

Creating a recursive function using NodeJS

This particular challenge I am tackling is quite intricate. My objective is to develop a recursive function in NodeJS that can interact with the database to retrieve results. Based on the retrieved data, the function should then recursively call itself. F ...

After clicking, revert back to the starting position

Hey there, I have a question about manipulating elements in the DOM. Here's the scenario: when I click a button, a div is displayed. After 2 seconds, this div is replaced by another one which has a function attached to it that removes the div again. ...

Having trouble transmitting data with axios between React frontend and Node.js backend

My current challenge involves using axios to communicate with the back-end. The code structure seems to be causing an issue because when I attempt to access req.body in the back-end, it returns undefined. Here is a snippet of my front-end code: const respo ...

Using Vuex: Delay dispatch of action until websocket response received

Let's look at the given scenario and premises: To populate a chat queue in real time, it is necessary to establish a connection to a websocket, send a message, and then store the data in a websocket store. This store will handle all the websocket sta ...

Effective techniques for unit testing in Vue.js

Here's a question that's been on my mind: when it comes to unit testing in Vue.js, there are several different packages available. Vue Test Utils Vue Jest Vue Cypress For enterprise projects, which of these options would be considered best ...

VueJS encountered an error during rendering: TypeError - Unable to access the 'PRICE' property of undefined

My goal is to calculate the total bill, which is the sum of all product prices multiplied by their respective usage values, using the customerProduct data object. Once calculated, I want to display the bill amount. To achieve this, I have a computed method ...

Developed a new dynamic component in VUE that is functional, but encountered a warning stating "template or render function not defined."

I'm currently working on a dynamic markdown component setup that looks like this <div v-highlight :is="markdownComponent"></div> Here's the computed section: computed: { markdownComponent() { return { temp ...

Using AngularJS to filter JSON data

Greetings! I possess the following JSON data: $scope.Facilities= [ { Name: "-Select-", Value: 0, RegionNumber: 0 }, { Name: "Facility1", Value: 1, RegionNumber: 1 }, { Name: ...

Instead of automatically playing, Youtube videos either remain idle or display suggested videos

I am trying to play a specific moment of an embedded Youtube video using some javascript code. At the specified time, I execute the following code: document.getElementById("video").src= "https://www.youtube.com/embed/...?autoplay=1&start=212"; where ...

"Learn how to create a scrolling div using a combination of CSS and JavaScript with absolute and relative

After relying solely on "pre-made" components like Mui or TailWind, I decided to create a component using only CSS and maybe JavaScript. However, I encountered some difficulties when attempting to position a div inside an image using relative and absolute ...

Leveraging data schemas to manage the feedback from APIs

I am curious about the benefits of modeling the API response on the client side. Specifically: First scenario: const [formData, setFormData] = useState(null); ... useEffect(() => { const callback = async () => { try { const fetchDa ...

Tips for utilizing the material ui auto-complete search feature

I am in search of an alternative to material-ui-search-bar because it is no longer being maintained. I have been suggested to try using Material UI's auto complete instead. However, from the examples I've seen, it seems like only text field struc ...