├── .eslintignore
├── .babelrc
├── src
├── utils
│ ├── warn.js
│ ├── string.js
│ └── dom.js
├── a11y.js
├── head
│ └── index.js
└── body
│ └── index.js
├── .gitignore
├── .editorconfig
├── .eslintrc
├── webpack.config.js
├── index.html
├── .github
└── ISSUE_TEMPLATE
│ ├── --report-bug.md
│ └── ----feature-request.md
├── LICENSE
├── package.json
├── docs
└── rules.md
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/warn.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console*/
2 | const Warning = message => console.warn(message);
3 |
4 | export {Warning};
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Directories
2 | dist
3 | node_modules
4 |
5 | # Files
6 | package-lock.json
7 |
8 | # OSX files
9 | .DS_Store
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/src/utils/string.js:
--------------------------------------------------------------------------------
1 | const toLower = string => string.toLowerCase();
2 | const isEmpty = string => string.trim() === '';
3 | const isNull = element => element === null;
4 |
5 | export {toLower, isEmpty, isNull};
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "indent": ["error", 2]
4 | },
5 | "env": {
6 | "es6": true,
7 | "browser": true,
8 | "node": true
9 | },
10 | "parser": "babel-eslint",
11 | "parserOptions": {
12 | "ecmaVersion": 6,
13 | "sourceType": "module"
14 | },
15 | "extends": "eslint:recommended"
16 | }
17 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: __dirname + '/src/a11y.js',
3 | output: {
4 | path: __dirname + '/dist',
5 | publicPath: '/dist/',
6 | filename: 'a11y.js',
7 | library: 'a11yChecker',
8 | libraryTarget: 'umd',
9 | umdNamedDefine: true
10 | },
11 | module: {
12 | rules: [{
13 | test: /\.js$/,
14 | exclude: /node_modules/,
15 | use: 'babel-loader'
16 | }]
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | a11y-checker
7 | Check console for details
8 |
9 |
10 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/--report-bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41BvReport Bug"
3 | about: Report an issue
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/----feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: " \U0001F31F Feature request"
3 | about: Request a new feature to be added
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I cannot be able to check....
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/a11y.js:
--------------------------------------------------------------------------------
1 | import * as head from './head';
2 | import * as body from './body';
3 |
4 | const a11yChecker = () => {
5 | head.hasDocumentType();
6 | head.hasDocumentTitle();
7 | head.hasDocumentLanguage();
8 | head.hasDocumentMetaCharset();
9 | head.hasDocumentScalable();
10 |
11 | body.hasHeadingOnce();
12 | body.hasImagesAlt();
13 | body.hasLinksText();
14 | body.hasLinksHref();
15 | body.hasLinksTarget();
16 | body.hasButtonsText();
17 | body.hasSVGRole();
18 | body.hasIframeTitle();
19 | body.hasFormsLabel();
20 | body.hasForLabel();
21 | body.hasVideoTrack();
22 | body.hasAudioTrack();
23 | body.hasPositiveTabIndex();
24 | body.hasDuplicateIds();
25 | };
26 |
27 | export default a11yChecker;
28 |
--------------------------------------------------------------------------------
/src/utils/dom.js:
--------------------------------------------------------------------------------
1 | const getElement = element => document.querySelector(element);
2 | const getElements = element => document.querySelectorAll(element);
3 |
4 | const hasAttribute = (element, attribute) => element.hasAttribute(attribute);
5 | const getAttribute = (element, attribute) => element.getAttribute(attribute);
6 |
7 | const doctype = document.doctype;
8 | const title = document.title;
9 |
10 |
11 | const hasAccessibileText = element => hasAttribute(element, 'aria-label') || hasAttribute(element, 'aria-labelledby');
12 |
13 | const hasTrack = track => track.textTracks.length === 0;
14 |
15 | export {
16 | getElement, getElements,
17 | hasAttribute, getAttribute,
18 | doctype, title,
19 | hasAccessibileText, hasTrack
20 | };
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Muhannad Abdelrazek
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "a11y-checker",
3 | "version": "2.2.1",
4 | "description": "Identifies accessibility issues in your code.",
5 | "main": "./src/a11y.js",
6 | "scripts": {
7 | "start": "npm-run-all --parallel dev:server lint:watch",
8 | "dev:server": "webpack-dev-server --hot --inline",
9 | "watch": "webpack -w -d",
10 | "build": "webpack -p",
11 | "lint": "node_modules/.bin/esw webpack.config.* src --color",
12 | "lint:watch": "npm run lint -- --watch"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/Muhnad/a11y-checker.git"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/Muhnad/a11y-checker/issues"
20 | },
21 | "keywords": [
22 | "a11y",
23 | "accessibility",
24 | "developers",
25 | "tool",
26 | "testing",
27 | "report"
28 | ],
29 | "author": "Muhannad Abdelrazek",
30 | "license": "MIT",
31 | "devDependencies": {
32 | "babel-core": "^6.24.1",
33 | "babel-eslint": "^7.2.3",
34 | "babel-loader": "^6.4.1",
35 | "babel-preset-es2015": "^6.24.1",
36 | "eslint": "^3.19.0",
37 | "eslint-watch": "^2.1.14",
38 | "npm-run-all": "^4.0.2",
39 | "webpack": "^2.4.1",
40 | "webpack-dev-server": "^2.4.2"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/head/index.js:
--------------------------------------------------------------------------------
1 | import {Warning} from '../utils/warn';
2 | import {isEmpty} from '../utils/string';
3 | import {
4 | getElement, getElements,
5 | hasAttribute, getAttribute,
6 | doctype, title
7 | } from '../utils/dom';
8 |
9 |
10 | const hasDocumentType = () => {
11 | if (!doctype) Warning('Doctype is missing. Fix: Add ');
12 | };
13 |
14 |
15 | const hasDocumentTitle = () => {
16 | if (isEmpty(title)) Warning('Title is missing. Fix: WELL DESCRIBED TITLE');
17 | };
18 |
19 |
20 | const hasDocumentLanguage = () => {
21 | const HTML = getElement('html');
22 | const hasLanguageAttr = hasAttribute(HTML, 'lang');
23 |
24 | if (hasLanguageAttr) {
25 | const getLanguageValue = getAttribute(HTML, 'lang');
26 | const isLanguageValueNotExist = isEmpty(getLanguageValue);
27 |
28 | if (isLanguageValueNotExist) Warning('Language value is missing in HTML element. Fix: Add lang="LANGUAGE VALUE" to ');
29 | } else {
30 | Warning('Language is missing in HTML element. Fix: Add lang="LANGUAGE VALUE" to ');
31 | }
32 | };
33 |
34 |
35 | const hasDocumentMetaCharset = () => {
36 | const META = [...getElements('meta')];
37 | const hasMetaCharset = META.some(tag => hasAttribute(tag, 'charset'));
38 |
39 | if (!hasMetaCharset) Warning('Document encoding is missing. Fix: Add ');
40 | };
41 |
42 |
43 | const hasDocumentScalable = () => {
44 | const META = [...getElements('meta')];
45 | const hasMetaScalable = META.some(el => getAttribute(el, 'user-scalable') === 'no');
46 |
47 | if (hasMetaScalable) Warning('Document must not use the user-scalable=no. Fix: Remove user-scalable=no from ');
48 | };
49 |
50 | export {
51 | hasDocumentType, hasDocumentLanguage,
52 | hasDocumentTitle, hasDocumentMetaCharset, hasDocumentScalable
53 | };
54 |
--------------------------------------------------------------------------------
/docs/rules.md:
--------------------------------------------------------------------------------
1 | # A11y Checker Rules
2 |
3 | - Document type: [Doctype syntax](https://w3c.github.io/html/syntax.html#the-doctype)
4 |
5 | - Page title: [Web pages have titles that describe topic or purpose](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#navigation-mechanisms-title)
6 |
7 | - Document Language: [The default human language of each Web page can be programmatically determined](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#meaning-doc-lang-id)
8 |
9 | - Document encoding: [Declaring character encodings in HTML ](https://www.w3.org/International/questions/qa-html-encoding-declarations)
10 |
11 | - Document scalable: [Resize text](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-scale.html)
12 |
13 | - Heading: [Headings and sections](https://www.w3.org/TR/2014/REC-html5-20141028/sections.html#headings-and-sections)
14 |
15 | - Images: [Every image must have an alt attribute](https://webaim.org/techniques/alttext/)
16 |
17 | - Links: [Providing context for the links](https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G53)
18 |
19 | - Buttons: [Providing descriptive text for buttons](https://www.w3.org/TR/WCAG20-TECHS/FLASH27.html)
20 |
21 | - Forms: [Providing descriptive label for forms](https://www.w3.org/TR/2014/NOTE-WCAG20-TECHS-20140408/H44)
22 |
23 | - Iframes: [Providing descriptive title for iframes](https://www.w3.org/TR/2014/NOTE-WCAG20-TECHS-20140408/H64)
24 |
25 | - Audios: [Providing an alternative for time-based media for audio-only content](https://www.w3.org/TR/WCAG20-TECHS/G158.html)
26 |
27 | - Videos: [Providing an alternative for time-based media for video-only content](https://www.w3.org/TR/WCAG20-TECHS/G159.html)
28 |
29 | - Positive tabindex value: [Focus order](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#navigation-mechanisms-focus-order)
30 | - Unique IDs: [the id attribute value must be unique](https://www.w3.org/TR/html5/dom.html#ref-for-concept-id⑦)
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # a11y checker
2 | > Identifies accessibility issues in HTML markup.
3 |
4 |
5 | ## Install
6 |
7 | ```bash
8 | npm install --save a11y-checker
9 | ```
10 |
11 | ## Usage
12 |
13 | - Import a11yChecker
14 | ```js
15 | import a11yChecker from 'a11y-checker';
16 | ```
17 |
18 | - Call it after page loads:
19 | ```js
20 | a11yChecker();
21 | ```
22 |
23 | ## Live Examples
24 | - [JS Live Example](https://oozqxkw1lz.codesandbox.io/)
25 | - [JS Code Example](https://codesandbox.io/s/oozqxkw1lz)
26 | - [React Live Example](https://9yly237pkr.codesandbox.io/)
27 | - [React Code Example](https://codesandbox.io/s/9yly237pkr)
28 | - [Vue Live Example](https://4jr02o9ln9.codesandbox.io/)
29 | - [Vue Code Example](https://codesandbox.io/s/4jr02o9ln9)
30 |
31 |
32 |
33 |
34 | ## Contributing
35 |
36 | > Hey there! Thanks for your interest in helping out. If you happen to
37 | > run into any issues, please
38 | > [open an issue](https://github.com/Muhnad/a11y-checker/issues/new/choose),
39 | > and I'll do my best to help out.
40 |
41 | To begin contributing, you'll first need to
42 | [clone this repository](https://help.github.com/articles/cloning-a-repository/),
43 | then navigate into the repository's directory.
44 |
45 | ```
46 | git clone git@github.com:{{ YOUR_USERNAME }}/a11y-checker.git
47 |
48 | cd a11y-checker/
49 | ```
50 |
51 | Next, install the dependencies using [npm](https://www.npmjs.com/).
52 |
53 | ```
54 | npm install
55 | ```
56 | Great! – you're ready to contribute!
57 |
58 | Create git branch
59 | ```
60 | git checkout -b BRANCH_NAME_HERE
61 | ```
62 |
63 | Run code locally. To do that, execute the start command:
64 |
65 | commands | Description
66 | --------------- | ----------
67 | `npm start` | Run project locally on port=8080.
68 | `npm build` | Generate a minified, production-ready build.
69 |
70 |
71 | ## Files structure
72 | Folder | Description
73 | --------------- | ----------
74 | src | for development files.
75 | head | for check everything happens inside ``
76 | body | for check everything happens inside ``
77 |
78 |
79 | ## Rules
80 | [Docs](https://github.com/Muhnad/a11y-checker/tree/master/docs)
81 |
82 | ## Tools
83 | there's a lot of a11y linters and tools that work and maintained better than A11y-Checker.
84 |
85 | Tools | Description
86 | --------------- | ----------
87 | [eslint-jsx](https://github.com/evcohen/eslint-plugin-jsx-a11y) | Static AST checker for a11y rules on JSX elements.
88 | [axe-core](https://github.com/dequelabs/axe-core) | Generate a minified, production-ready build.
89 | [ally.js](https://github.com/medialize/ally.js) | JavaScript library to help modern web applications with accessibility concerns
90 | [Awesome-a11y-validators](https://github.com/brunopulis/awesome-a11y/blob/master/topics/validators.md) | List of development Testing and Validators tools.
91 |
92 | That's All. Thanks.
93 |
--------------------------------------------------------------------------------
/src/body/index.js:
--------------------------------------------------------------------------------
1 | import {Warning} from '../utils/warn';
2 | import {isEmpty, isNull} from '../utils/string';
3 | import {
4 | getElements, hasAttribute, getAttribute,
5 | hasAccessibileText, hasTrack
6 | } from '../utils/dom';
7 |
8 |
9 | const hasHeadingOnce = () => {
10 | const H1 = getElements('h1');
11 | const hasMultiHeading = H1.length > 1;
12 |
13 | if (hasMultiHeading) Warning('Page has Multi tag. Fix: use only one in the page.');
14 | };
15 |
16 |
17 | const hasImagesAlt = () => {
18 | const IMGS = [...getElements('img')];
19 | const imagesWithoutAlt = IMGS.filter(img => !hasAttribute(img, 'alt'));
20 | const hasMissingAlt = imagesWithoutAlt.length > 0;
21 | const withoutAltWarning = imagesWithoutAlt.forEach(image => Warning(`Image Alt is missing. Fix: Add alt="IMAGE WELL DESCRIBED" to ${image.outerHTML}`));
22 |
23 | if (hasMissingAlt) withoutAltWarning;
24 | };
25 |
26 | const hasLinksText = () => {
27 | const LINKS = [...getElements('a')];
28 | const warningMessage = 'Link text is missing. Fix: DESCRIBE PURPOSE OF LINK';
29 | const linksWithoutText = LINKS.filter(link => isEmpty(link.textContent) && !hasAccessibileText(link));
30 | const hasMissingText = linksWithoutText.length > 0;
31 | const withoutTextWarning = linksWithoutText.forEach(link => Warning(`${warningMessage} to ${link.outerHTML}`));
32 |
33 | if (hasMissingText) withoutTextWarning;
34 | };
35 |
36 |
37 | const hasLinksHref = () => {
38 | const LINKS = [...getElements('a')];
39 | const linksWithoutHref = LINKS.filter(link => (!hasAttribute(link, 'href') || isEmpty(getAttribute(link, 'href'))) && !hasAttribute(link, 'role'));
40 | const hasMissingHref = linksWithoutHref.length > 0;
41 | const withoutHrefWarning = linksWithoutHref.forEach(link => Warning(`Link Href is missing. Fix: Add href="LINK URL" to ${link.outerHTML}`));
42 |
43 | if (hasMissingHref) withoutHrefWarning;
44 | };
45 |
46 |
47 | const hasLinksTarget = () => {
48 | const LINKS = [...getElements('a')];
49 | const warningMessage = 'Hint message is missing. Should add hint message to recognize this link will open in new tab. Fix: Add aria-describedby="ELEMENT ID"';
50 | const linksWithTarget = LINKS.filter(link => getAttribute(link, 'target') === '_blank' && !hasAttribute(link, 'aria-describedby'));
51 | const hasTarget = linksWithTarget.length > 0;
52 | const missingTargetHint = linksWithTarget.forEach(link => Warning(`${warningMessage} to ${link.outerHTML}`));
53 |
54 | if (hasTarget) missingTargetHint;
55 | };
56 |
57 |
58 | const hasButtonsText = () => {
59 | const BUTTONS = [...getElements('button')];
60 | const warningMessage = 'Button text or aria-label is missing. Fix: Add aria-label="VALUE" or ';
61 | const buttonsWithoutText = BUTTONS.filter(button => isEmpty(button.textContent) && !hasAccessibileText(button));
62 | const hasMissingText = buttonsWithoutText.length > 0;
63 | const withoutTextWarning = buttonsWithoutText.forEach(button => Warning(`${warningMessage} to ${button.outerHTML}`));
64 |
65 | if (hasMissingText) withoutTextWarning;
66 | };
67 |
68 |
69 | const hasForLabel = () => {
70 | const LABELS = [...getElements('label')];
71 | const isLabeld = label => {
72 | if (!hasAttribute(label, 'for') || isEmpty(getAttribute(label, 'for')))
73 | Warning(`For is missing in label. Fix: Add for="INPUT ID" to ${label.outerHTML}`);
74 | };
75 | const missingForLabel = LABELS.forEach(isLabeld);
76 |
77 | return missingForLabel;
78 | };
79 |
80 |
81 | const hasSVGRole = () => {
82 | const SVGS = [...getElements('SVG')];
83 | const hasMissingRole = SVGS.some(svg => getAttribute(svg, 'aria-hidden') !== 'true' && !hasAttribute(svg, 'role') && !getAttribute(svg, 'id'));
84 |
85 | if (hasMissingRole) Warning('SVG Role is missing. Fix: Add role="img" or (aria-hidden="true" if you need to hide element from SR).');
86 | };
87 |
88 |
89 | const hasIframeTitle = () => {
90 | const IFRAMES = [...getElements('iframe')];
91 | const iframeWithoutTitle = IFRAMES.some(ifrmae => !hasAttribute(ifrmae, 'title'));
92 |
93 | if (iframeWithoutTitle) Warning('Title is missing in iframe. Fix: Add title="DESCRIBE CONTENT OF FRAME" to