Improve your Vue 3 accessiblity with linters & git hooks

Published

November 13, 2022

In this post, we'll look into adding accessibility linters to your Vue 3 project. This can be your regular Vue 3 app or a Nuxt 3 website or any other environment. Adding accessibility linters can be a great first step to improving the accessibility of your app or website. Of course, you should always learn about different accessibility topics and there is no one size fits all solution, but linters can be a great baseline because they are enforced while you're writing you're code. With git hooks we can ensure that no new code is added, that doesn't follow these linters and it can also be useful for junior developers to learn when they might be doing something wrong. In this post we'll look into adding the following tools & linters:

Creating a new Vue 3 app

First, we'll create a new Vue 3 app. The Vue quickstart makes it very easy to create a new project.

npm init vue@latest

Make sure to directly install the ESLint and Prettier plugins, because they will help us later on.

✔ Add ESLint for code quality? - Yes
✔ Add Prettier for code formatting? - Yes

Now we can directly jump into our new project and it should already show the welcome screen.

cd vue-project
npm install
npm run dev

Adding the ESLint accessibility plugin

The next step is to extend our ESLint setup to include a plugin for a11y. Since we scaffolded our project with ESLint, we should have a .eslintrc.cjs file in our project with some standard plugins installed. Let's take a look at it:

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true, // This is required to prevent ESLint from looking for a configuration file in parent folders
  'extends': [
    'plugin:vue/vue3-essential', // vue 3 linting
    'eslint:recommended', // standard eslint javascript linting
    '@vue/eslint-config-typescript', // typescript linting
    '@vue/eslint-config-prettier' // prettier linting
  ],
  parserOptions: {
    ecmaVersion: 'latest' // Allows for the parsing of modern ECMAScript features
  }
}

So let's add the eslint-plugin-vuejs-accessibility plugin to our project. We can do this by running the following command:

npm install --save-dev eslint-plugin-vuejs-accessibility

And then we need to add it to our .eslintrc.cjs file:

module.exports = {
  root: true,
  'plugins': ['vuejs-accessibility'], // add the plugin
  'extends': [
    "plugin:vuejs-accessibility/recommended", // add the recommended rules
    ...
  ],
  parserOptions: { ... }
}

After this, we can run our linter and see if we have any accessibility issues in our project. Since we just scaffolded the project there are probably no issues. We can run the linter with the following command:

npm run lint

Perfect, no issues! Let's add some accessibility issues to our project and see if the linter can catch them. Open your App.vue file and remove the alt attribute from the image tag. The linter should now show an error:

<header>
    <img
        alt="Vue logo" // <--- remove this line
        class="logo"
        src="./assets/logo.svg"
        width="125"
        height="125"
    />

    <div class="wrapper">
        <HelloWorld msg="You did it!" />
    </div>
</header>

When running npm run lint again, we should see the following error:

✖ error  img elements must have an alt prop, either with meaningful text, or an empty string for decorative images  vuejs-accessibility/alt-text

We can see more details for the specific rules in the docs of the different plugin rules like this one: vuejs-accessibility/alt-text

Adapting the linter

It's also possible that you need to activate a specific rule or set options for a rule, because your project has specific needs. You can do this by adding a rules section to your .eslintrc.cjs file:

module.exports = {
  root: true,
  'plugins': ['vuejs-accessibility'],
  'extends': [
    "plugin:vuejs-accessibility/recommended",
    ...
  ],
  'rules': {
    'vuejs-accessibility/alt-text': ['error', { // <--- this would change the options of the rule
      'elements': ['img'], // <--- e.g. only lint images, but no other HTML elements
      'img': [],
      'object': [],
      'area': [],
      'input[type="image"]': []
    }],
    "vuejs-accessibility/no-autofocus": "off", // <--- this would disable this rule
  },
  parserOptions: { ... }
}

Adding stylelint and the stylelint-a11y plugin

Next we also want to lint our CSS, because we can also have accessibility issues there. We can do this by adding the stylelint linter to our project. The configuration there depends on what CSS processing you're using. For this case, we'll use CSS modules:

npm install --save-dev postcss-html stylelint stylelint-config-recommended-vue stylelint-config-standard stylelint-config-prettier  stylelint-config-css-modules @ronilaukkarinen/stylelint-a11y

Let's take a look at these packages:

Adapting the stylelint configuration

After installing the packages, we can create a new .stylelintrc.js file in the root of our project and add the following config:

module.exports = {
  extends: [
    "stylelint-config-standard",
    "stylelint-config-prettier",
    "stylelint-config-recommended-vue",
    "stylelint-config-css-modules",
    "@ronilaukkarinen/stylelint-a11y/recommended",
  ],
};

Let's also add a new script to our package.json file to run the linter. By adding --fix in the end, it would try to autofix the issues:

{
  "scripts": {
     "lint:style": "stylelint 'src/**/*.{vue,css}' --fix" 
  }
}

Now we can run stylelint to see if we have any issues in our CSS:

npm run lint:style

When running npm run lint:styles again, we should already see an error:

✖  Unexpected duplicate selector ":root", first used at line 2  no-duplicate-selectors

The stylelint plugin only activates three rules by default, but we can enable more rules by adapting the .stylelintrc.js file:

module.exports = {
    extends: [
        "stylelint-config-standard",
        "stylelint-config-prettier",
        "stylelint-config-recommended-vue",
        "stylelint-config-css-modules",
        "@ronilaukkarinen/stylelint-a11y/recommended",
    ],
    rules: {
        "a11y/no-text-align-justify": true,
        "a11y/font-size-is-readable": true,
        "a11y/media-prefers-color-scheme": true,
        "a11y/line-height-is-vertical-rhythmed": true,
        "a11y/no-display-none": true,
        "a11y/no-obsolete-element": true,
    },
};

Let's try breaking some of those rules. Open the src/App.vue file and add the following CSS:

.wrapper {
  line-height: 1.3;
  outline: none;
  text-align: justify;
  font-size: 12px;
}

This should trigger some more errors:

✖  Expected a larger font-size in .wrapper               a11y/font-size-is-readable
✖  Expected a vertical rhythmed line-height in .wrapper  a11y/line-height-is-vertical-rhythmed

Adding a pre-commit hook to prevent committing code with accessibility issues

Now that we have a linter that checks our code for accessibility issues, we can add a pre-commit hook to prevent committing code with accessibility issues. We can do this by using the lint-staged package and husky. Let's install these packages:

npm install --save-dev lint-staged husky

Adding lint-staged

We can add a new lint-staged config to our package.json file to run the linters on the staged files:

{
  "lint-staged": {
    "*.{js,ts,vue}": [
      "npm run lint"
    ],
    "*.{css,vue}": [
      "npm run lint:style"
    ]
  }
}

Installing husky

Next we need to run lint-staged whenever we commit files. Husky is a tool that allows us to run scripts at certain git events. We can use it to run a script before we commit our code. The following command will setup husky, modify our package.json and create a sample pre-commit hook that you can edit. By default, it will run npm test when you commit.

git init # if you haven't initialized git yet
npx husky-init && npm install

After running the command, you should see a new .husky folder in the root of your project. In that folder, you'll find a pre-commit file. Let's edit that file and replace npm test with the following:

npx lint-staged

Now you can try commiting some code with accessibility issues and see that the commit fails.

git add .
git commit -m "test"

Lint staged should run the linters and prevent you from committing code with accessibility issues.

✔ Preparing lint-staged...
✔ Hiding unstaged changes to partially staged files...
❯ Running tasks for staged files...
  ❯ package.json — 31 files
    ❯ *.{js,ts,vue} — 14 files
      ✖ npm run lint [KILLED]
    ❯ *.{css,vue} — 11 files
      ✖ npm run lint:style [FAILED]
✔ Applying modifications from tasks...
↓ Skipped because of errors from tasks. [SKIPPED]

Conclusion

In this article, we learned how to add accessibility linting to our Vue 3 project. By adding pre-commit hooks we can prevent committing code with obvious accessibility issues. This is a perfect starting point for a new project, where multiple developers are working on the same codebase trying to make it accessible from the beginning. If you want to learn more about accessibility, here are some great resources:

More articles