├── .appveyor.yml ├── .editorconfig ├── .gitignore ├── .tape.js ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── index.js ├── package.json └── test ├── alpha.css ├── basic.css ├── basic.expect.css ├── comment.css ├── sized.css └── weighted.css /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # https://www.appveyor.com/docs/appveyor-yml 2 | 3 | environment: 4 | matrix: 5 | - nodejs_version: 4.0 6 | 7 | version: "{build}" 8 | build: off 9 | deploy: off 10 | 11 | install: 12 | - ps: Install-Product node $env:nodejs_version 13 | - npm install --ignore-scripts 14 | 15 | test_script: 16 | - node --version 17 | - npm --version 18 | - cmd: "npm test" 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.{json,md,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .* 3 | !.appveyor.yml 4 | !.editorconfig 5 | !.gitignore 6 | !.tape.js 7 | !.travis.yml 8 | *.log* 9 | *.result.css 10 | -------------------------------------------------------------------------------- /.tape.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'postcss-wcag-contrast': { 3 | 'basic': { 4 | message: 'supports basic usage', 5 | warning: 2, 6 | expect: 'basic.css' 7 | }, 8 | 'basic:aaa': { 9 | message: 'supports basic usage { compliance "AAA" }', 10 | warning: 4, 11 | options: { 12 | compliance: 'AAA' 13 | }, 14 | expect: 'basic.css' 15 | }, 16 | 'sized': { 17 | message: 'supports font-size usage', 18 | warning: 3, 19 | expect: 'sized.css' 20 | }, 21 | 'sized:aaa': { 22 | message: 'supports font-size usage with { compliance "AAA" }', 23 | warning: 3, 24 | options: { 25 | compliance: 'AAA' 26 | }, 27 | expect: 'sized.css' 28 | }, 29 | 'weighted': { 30 | message: 'supports font-size & font-weight usage', 31 | warning: 2, 32 | expect: 'weighted.css' 33 | }, 34 | 'weighted:aaa': { 35 | message: 'supports font-size & font-weight usage { compliance "AAA" }', 36 | warning: 3, 37 | options: { 38 | compliance: 'AAA' 39 | }, 40 | expect: 'weighted.css' 41 | }, 42 | 'comment': { 43 | message: 'supports wcag-param usage', 44 | warning: 3, 45 | expect: 'comment.css' 46 | }, 47 | 'comment:aaa': { 48 | message: 'supports wcag-param usage', 49 | warning: 4, 50 | options: { 51 | compliance: 'AAA' 52 | }, 53 | expect: 'comment.css' 54 | }, 55 | 'alpha': { 56 | message: 'supports alpha transparancy', 57 | warning: 4, 58 | expect: 'alpha.css' 59 | }, 60 | 'alpha:aaa': { 61 | message: 'supports alpha transparancy', 62 | warning: 6, 63 | options: { 64 | compliance: 'AAA' 65 | }, 66 | expect: 'alpha.css' 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/travis-lint 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 4 7 | 8 | install: 9 | - npm install --ignore-scripts 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes to WCAG Contrast 2 | 3 | ### 2.0.0 (May 30, 2017) 4 | 5 | - Added: Support for PostCSS v6 6 | - Added: Support for Node v4 7 | 8 | ### 1.1.0 (February 4, 2017) 9 | 10 | - Added: Support for alpha transparency 11 | 12 | ### 1.0.0 (February 19, 2016) 13 | 14 | - Added: Initial version 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to WCAG Contrast 2 | 3 | You want to help? You rock! Now, take a moment to be sure your contributions 4 | make sense to everyone else. 5 | 6 | ## Reporting Issues 7 | 8 | Found a problem? Want a new feature? 9 | 10 | - See if your issue or idea has [already been reported]. 11 | - Provide a [reduced test case] or a [live example]. 12 | 13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code. 14 | 15 | ## Submitting Pull Requests 16 | 17 | Pull requests are the greatest contributions, so be sure they are focused in 18 | scope and avoid unrelated commits. 19 | 20 | 1. To begin; [fork this project], clone your fork, and add our upstream. 21 | ```bash 22 | # Clone your fork of the repo into the current directory 23 | git clone git@github.com:YOUR_USER/postcss-wcag-contrast.git 24 | 25 | # Navigate to the newly cloned directory 26 | cd postcss-wcag-contrast 27 | 28 | # Assign the original repo to a remote called "upstream" 29 | git remote add upstream git@github.com:jonathantneal/postcss-wcag-contrast.git 30 | 31 | # Install the tools necessary for testing 32 | npm install 33 | ``` 34 | 35 | 2. Create a branch for your feature or fix: 36 | ```bash 37 | # Move into a new branch for your feature 38 | git checkout -b feature/thing 39 | ``` 40 | ```bash 41 | # Move into a new branch for your fix 42 | git checkout -b fix/something 43 | ``` 44 | 45 | 3. If your code follows our practices, then push your feature branch: 46 | ```bash 47 | # Test current code 48 | npm test 49 | ``` 50 | ```bash 51 | # Push the branch for your new feature 52 | git push origin feature/thing 53 | ``` 54 | ```bash 55 | # Or, push the branch for your update 56 | git push origin update/something 57 | ``` 58 | 59 | That’s it! Now [open a pull request] with a clear title and description. 60 | 61 | [already been reported]: issues 62 | [fork this project]: fork 63 | [live example]: https://codepen.io/pen 64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, 34 | communicate, and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data 41 | in a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes 60 | the Waiver for the benefit of each member of the public at large and to the 61 | detriment of Affirmer’s heirs and successors, fully intending that such Waiver 62 | shall not be subject to revocation, rescission, cancellation, termination, or 63 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 64 | by the public as contemplated by Affirmer’s express Statement of Purpose. 65 | 66 | 3. Public License Fallback. Should any part of the Waiver for any reason be 67 | judged legally invalid or ineffective under applicable law, then the Waiver 68 | shall be preserved to the maximum extent permitted taking into account 69 | Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver 70 | is so judged Affirmer hereby grants to each affected person a royalty-free, non 71 | transferable, non sublicensable, non exclusive, irrevocable and unconditional 72 | license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in 73 | all territories worldwide, (ii) for the maximum duration provided by applicable 74 | law or treaty (including future time extensions), (iii) in any current or 75 | future medium and for any number of copies, and (iv) for any purpose 76 | whatsoever, including without limitation commercial, advertising or promotional 77 | purposes (the “License”). The License shall be deemed effective as of the date 78 | CC0 was applied by Affirmer to the Work. Should any part of the License for any 79 | reason be judged legally invalid or ineffective under applicable law, such 80 | partial invalidity or ineffectiveness shall not invalidate the remainder of the 81 | License, and in such case Affirmer hereby affirms that he or she will not (i) 82 | exercise any of his or her remaining Copyright and Related Rights in the Work 83 | or (ii) assert any associated claims and causes of action with respect to the 84 | Work, in either case contrary to Affirmer’s express Statement of Purpose. 85 | 86 | 4. Limitations and Disclaimers. 87 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 88 | surrendered, licensed or otherwise affected by this document. 89 | 2. Affirmer offers the Work as-is and makes no representations or 90 | warranties of any kind concerning the Work, express, implied, statutory 91 | or otherwise, including without limitation warranties of title, 92 | merchantability, fitness for a particular purpose, non infringement, or 93 | the absence of latent or other defects, accuracy, or the present or 94 | absence of errors, whether or not discoverable, all to the greatest 95 | extent permissible under applicable law. 96 | 3. Affirmer disclaims responsibility for clearing rights of other persons 97 | that may apply to the Work or any use thereof, including without 98 | limitation any person’s Copyright and Related Rights in the Work. 99 | Further, Affirmer disclaims responsibility for obtaining any necessary 100 | consents, permissions or other rights required for any use of the Work. 101 | 4. Affirmer understands and acknowledges that Creative Commons is not a 102 | party to this document and has no duty or obligation with respect to 103 | this CC0 or use of the Work. 104 | 105 | For more information, please see 106 | https://creativecommons.org/publicdomain/zero/1.0/. 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WCAG Contrast [PostCSS Logo][postcss] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![Linux Build Status][cli-img]][cli-url] 5 | [![Windows Build Status][win-img]][win-url] 6 | [![Gitter Chat][git-img]][git-url] 7 | 8 | [WCAG Contrast] checks CSS for [color contrast compliance] with 9 | [Web Content Accessibility Guidelines (WCAG) 2.0]. 10 | 11 | ```css 12 | .header { 13 | background-color: #444; 14 | color: #000; /* throws a warning for a low contrast of only 2.2 */ 15 | } 16 | 17 | .footer { 18 | /* wcag-params: bold 14pt #777 */ 19 | color: #000; /* throws no warning when text is bold 14pt and contrast is 4.7 */ 20 | } 21 | ``` 22 | 23 | ## Usage 24 | 25 | Add [WCAG Contrast] to your build tool: 26 | 27 | ```bash 28 | npm install postcss-wcag-contrast --save-dev 29 | ``` 30 | 31 | #### Node 32 | 33 | Use [WCAG Contrast] to process your CSS: 34 | 35 | ```js 36 | require('postcss-wcag-contrast').process(YOUR_CSS); 37 | ``` 38 | 39 | #### PostCSS 40 | 41 | Add [PostCSS] to your build tool: 42 | 43 | ```bash 44 | npm install postcss --save-dev 45 | ``` 46 | 47 | Use [WCAG Contrast] as a plugin: 48 | 49 | ```js 50 | postcss([ 51 | require('postcss-wcag-contrast')() 52 | ]).process(YOUR_CSS); 53 | ``` 54 | 55 | #### Gulp 56 | 57 | Add [Gulp PostCSS] to your build tool: 58 | 59 | ```bash 60 | npm install gulp-postcss --save-dev 61 | ``` 62 | 63 | Use [WCAG Contrast] in your Gulpfile: 64 | 65 | ```js 66 | var postcss = require('gulp-postcss'); 67 | 68 | gulp.task('css', function () { 69 | return gulp.src('./src/*.css').pipe( 70 | postcss([ 71 | require('postcss-wcag-contrast')() 72 | ]) 73 | ).pipe( 74 | gulp.dest('.') 75 | ); 76 | }); 77 | ``` 78 | 79 | #### Grunt 80 | 81 | Add [Grunt PostCSS] to your build tool: 82 | 83 | ```bash 84 | npm install grunt-postcss --save-dev 85 | ``` 86 | 87 | Use [WCAG Contrast] in your Gruntfile: 88 | 89 | ```js 90 | grunt.loadNpmTasks('grunt-postcss'); 91 | 92 | grunt.initConfig({ 93 | postcss: { 94 | options: { 95 | use: [ 96 | require('postcss-wcag-contrast')() 97 | ] 98 | }, 99 | dist: { 100 | src: '*.css' 101 | } 102 | } 103 | }); 104 | ``` 105 | 106 | ## Options 107 | 108 | ### compliance 109 | 110 | Type: `String` 111 | Default: `"AA"` 112 | 113 | The `compliance` option specifies the WCAG compliance the CSS will be evaluated 114 | against. 115 | 116 | ### wcag-params 117 | 118 | Type: `CSS Comment` 119 | 120 | The `wcag-params` specifies additional font size, font weight, background, and 121 | foreground information about the rule. 122 | 123 | [npm-url]: https://www.npmjs.com/package/postcss-wcag-contrast 124 | [npm-img]: https://img.shields.io/npm/v/postcss-wcag-contrast.svg 125 | [cli-url]: https://travis-ci.org/jonathantneal/postcss-wcag-contrast 126 | [cli-img]: https://img.shields.io/travis/jonathantneal/postcss-wcag-contrast.svg 127 | [win-url]: https://ci.appveyor.com/project/jonathantneal/postcss-wcag-contrast 128 | [win-img]: https://img.shields.io/appveyor/ci/jonathantneal/postcss-wcag-contrast.svg 129 | [git-url]: https://gitter.im/postcss/postcss 130 | [git-img]: https://img.shields.io/badge/chat-gitter-blue.svg 131 | 132 | [WCAG Contrast]: https://github.com/jonathantneal/postcss-wcag-contrast 133 | [PostCSS]: https://github.com/postcss/postcss 134 | [Gulp PostCSS]: https://github.com/postcss/gulp-postcss 135 | [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss 136 | 137 | [color contrast compliance]: https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast 138 | [Web Content Accessibility Guidelines (WCAG) 2.0]: https://www.w3.org/TR/WCAG20/ 139 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const onecolor = require('onecolor'); 4 | const postcss = require('postcss'); 5 | const comma = postcss.list.comma; 6 | const space = postcss.list.space; 7 | 8 | const cssColorRegExp = new RegExp('^(#(?:[\\da-f]{3}){1,2}|rgb\\((?:\\d{1,3},\\s*){2}\\d{1,3}\\)|rgba\\((?:\\d{1,3},\\s*){3}\\d*\\.?\\d+\\)|hsl\\(\\d{1,3}(?:,\\s*\\d{1,3}%){2}\\)|hsla\\(\\d{1,3}(?:,\\s*\\d{1,3}%){2},\\s*\\d*\\.?\\d+\\))$'); 9 | const cssLengthRegExp = new RegExp('^([-+]?0|[-+]?[0-9]*\.?[0-9]+(%|cm|in|mm|pc|pt|px|rem))$'); 10 | const cssWeightRegExp = new RegExp('^([1-9]00|bold|normal)$'); 11 | const cssParamsRegExp = new RegExp('^(\s*wcag-params:\s*(.+)\s*)$'); 12 | 13 | const cssLengthUnits = { 14 | cm: 0.3937 * 96, 15 | in: 96, 16 | mm: 0.3937 * 96 / 10, 17 | pc: 12 * 96 / 72, 18 | pt: 96 / 72, 19 | rem: 16 20 | }; 21 | 22 | module.exports = postcss.plugin('postcss-wcag-contrast', (opts) => { 23 | // set options 24 | const compliance = opts && 'compliance' in opts ? String(opts.compliance).toUpperCase() : 'AA'; 25 | 26 | if (compliance !== 'AAA' && compliance !== 'AA') { 27 | throw new Error('The `compliance` option must be "AA" or "AAA"'); 28 | } 29 | 30 | // return wcag contrast plugin 31 | return (css, result) => { 32 | // walk each rule 33 | css.walkRules((rule) => { 34 | let fontsize; 35 | let fontweight; 36 | 37 | let foreground; 38 | let background; 39 | let fallbackcolor; 40 | let fallbacksize; 41 | let fallbackweight; 42 | 43 | // for each comment or declration of the rule 44 | rule.nodes.forEach((node) => { 45 | if (node.prop === 'color') { 46 | foreground = node.value; 47 | } else if (node.prop === 'background-color') { 48 | background = node.value; 49 | } else if (node.prop === 'font-size') { 50 | fontsize = getFontSize(node.value); 51 | } else if (node.prop === 'font-weight') { 52 | fontweight = getFontWeight(node.value); 53 | } else if (node.prop === 'font') { 54 | fontweight = 400; 55 | 56 | space(node.value.replace(/\/[^\s]+/, '')).forEach((spaceSplitValue) => { 57 | if (cssWeightRegExp.test(spaceSplitValue)) { 58 | fontweight = getFontWeight(spaceSplitValue); 59 | } else if (cssLengthRegExp.test(spaceSplitValue)) { 60 | fontsize = getFontSize(spaceSplitValue); 61 | } 62 | }); 63 | } else if (node.prop === 'background') { 64 | // split background by comma and space 65 | comma(node.value).forEach((commaSplitValue) => { 66 | space(commaSplitValue).forEach((spaceSplitValue) => { 67 | // conditionally set the the background color 68 | if (cssColorRegExp.test(spaceSplitValue)) { 69 | background = spaceSplitValue; 70 | } 71 | }); 72 | }); 73 | } else if (node.type === 'comment') { 74 | if (cssParamsRegExp.test(node.text)) { 75 | // split comment by space 76 | space(node.text).forEach((spaceSplitValue) => { 77 | // conditionally define fallback values 78 | if (cssColorRegExp.test(spaceSplitValue)) { 79 | fallbackcolor = spaceSplitValue; 80 | } else if (cssWeightRegExp.test(spaceSplitValue)) { 81 | fallbackweight = getFontWeight(spaceSplitValue); 82 | } else if (cssLengthRegExp.test(spaceSplitValue)) { 83 | fallbacksize = getFontSize(spaceSplitValue); 84 | } 85 | }); 86 | } 87 | } 88 | }); 89 | 90 | // conditionally update foreground or background 91 | if (!foreground) { 92 | foreground = fallbackcolor; 93 | } else if (!background) { 94 | background = fallbackcolor; 95 | } 96 | 97 | // ignore rules without a foreground or background 98 | if (!foreground || !background) { 99 | return; 100 | } 101 | 102 | // conditionally update font size 103 | if (!fontsize) { 104 | fontsize = fallbacksize || 16; 105 | } 106 | 107 | // conditionally update font weight 108 | if (!fontweight) { 109 | fontweight = fallbackweight || 400; 110 | } 111 | 112 | const contrastRatio = getContrastRatio(foreground, background); 113 | 114 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#larger-scaledef 115 | const isLargeScale = fontsize >= 24 || fontsize >= 14 * (96 / 72) && fontweight >= 700; 116 | 117 | // conditionally warn of compliance 118 | if (compliance === 'AAA') { 119 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7 120 | const minAAAContrastRatio = isLargeScale ? 4.5 : 7; 121 | const hasAAAContrast = contrastRatio >= minAAAContrastRatio; 122 | 123 | if (!hasAAAContrast) { 124 | rule.warn(result, contrastRatio.toFixed(2) + ':1 is an insufficient contrast ratio (WCAG 2.0 AAA requires ' + minAAAContrastRatio + ':1)'); 125 | } 126 | } else if (compliance === 'AA') { 127 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast 128 | const minAAContrastRatio = isLargeScale ? 3 : 4.5; 129 | const hasAAContrast = contrastRatio >= minAAContrastRatio; 130 | 131 | if (!hasAAContrast) { 132 | rule.warn(result, contrastRatio.toFixed(2) + ':1 is an insufficient contrast ratio (WCAG 2.0 AA requires ' + minAAContrastRatio + ':1)'); 133 | } 134 | } 135 | }); 136 | }; 137 | }); 138 | 139 | function getRelativeLuminance(cssColor) { 140 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 141 | const color = onecolor(cssColor); 142 | 143 | const R = color._red <= 0.03928 ? color._red / 12.92 : Math.pow((color._red + 0.055) / 1.055, 2.4); 144 | const G = color._green <= 0.03928 ? color._green / 12.92 : Math.pow((color._green + 0.055) / 1.055, 2.4); 145 | const B = color._blue <= 0.03928 ? color._blue / 12.92 : Math.pow((color._blue + 0.055) / 1.055, 2.4); 146 | 147 | const L = 0.2126 * R + 0.7152 * G + 0.0722 * B; 148 | 149 | return L; 150 | } 151 | 152 | function alphaBlend(cssForeground, cssBackground) { 153 | const foreground = onecolor(cssForeground); 154 | const background = onecolor(cssBackground); 155 | const result = onecolor('#fff'); 156 | const a = foreground.alpha(); 157 | 158 | result._red = foreground._red * a + background._red * (1 - a); 159 | result._green = foreground._green * a + background._green * (1 - a); 160 | result._blue = foreground._blue * a + background._blue * (1 - a); 161 | 162 | return result; 163 | } 164 | 165 | function getContrastRatioOpaque(foreground, background) { 166 | const L1 = getRelativeLuminance(background); 167 | const L2 = getRelativeLuminance(alphaBlend(foreground, background)); 168 | 169 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef 170 | return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05); 171 | } 172 | 173 | function getContrastRatio(foreground, background) { 174 | const backgroundOnWhite = alphaBlend(background, '#fff'); 175 | const backgroundOnBlack = alphaBlend(background, '#000'); 176 | 177 | const LWhite = getRelativeLuminance(backgroundOnWhite); 178 | const LBlack = getRelativeLuminance(backgroundOnBlack); 179 | const LForeground = getRelativeLuminance(foreground); 180 | 181 | if (LWhite < LForeground) { 182 | return getContrastRatioOpaque(foreground, backgroundOnWhite); 183 | } else if (LBlack > LForeground) { 184 | return getContrastRatioOpaque(foreground, backgroundOnBlack); 185 | } else { 186 | return 1; 187 | } 188 | } 189 | 190 | function getFontSize(cssLength) { 191 | const fontSizeMatch = cssLength.match(cssLengthRegExp); 192 | 193 | const fontSize = fontSizeMatch && parseFloat(fontSizeMatch[0]); 194 | const fontSizeUnit = fontSizeMatch && fontSizeMatch[2]; 195 | 196 | const fontSizeScale = fontSizeUnit in cssLengthUnits ? cssLengthUnits[fontSizeUnit] : 1; 197 | 198 | return fontSize ? fontSize * fontSizeScale : 16; 199 | } 200 | 201 | function getFontWeight(cssWeight) { 202 | return cssWeight === 'bold' ? 700 : cssWeight === 'normal' ? 400 : parseInt(cssWeight) || 400; 203 | } 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-wcag-contrast", 3 | "version": "2.0.0", 4 | "description": "Check for WCAG color contrast compliance", 5 | "author": "Jonathan Neal ", 6 | "license": "CC0-1.0", 7 | "repository": "jonathantneal/postcss-wcag-contrast", 8 | "homepage": "https://github.com/jonathantneal/postcss-wcag-contrast#readme", 9 | "bugs": "https://github.com/jonathantneal/postcss-wcag-contrast/issues", 10 | "main": "index.js", 11 | "files": [ 12 | "index.js" 13 | ], 14 | "scripts": { 15 | "clean": "git clean -X -d -f", 16 | "prepublish": "npm test", 17 | "test": "echo 'Running tests...'; npm run test:js && npm run test:tape", 18 | "test:js": "eslint *.js --cache --ignore-pattern .gitignore", 19 | "test:tape": "postcss-tape" 20 | }, 21 | "engines": { 22 | "node": ">=4.0.0" 23 | }, 24 | "dependencies": { 25 | "onecolor": "^3.0.4", 26 | "postcss": "^6.0.1" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^3.19.0", 30 | "eslint-config-dev": "2.0.0", 31 | "postcss-tape": "2.0.1", 32 | "pre-commit": "^1.2.2" 33 | }, 34 | "eslintConfig": { 35 | "extends": "dev" 36 | }, 37 | "keywords": [ 38 | "postcss", 39 | "css", 40 | "postcss-plugin", 41 | "aa", 42 | "aaa", 43 | "analyse", 44 | "analyze", 45 | "blues", 46 | "colors", 47 | "compliances", 48 | "contrasts", 49 | "greens", 50 | "guards", 51 | "lints", 52 | "luminances", 53 | "luminosity", 54 | "minimum", 55 | "ratios", 56 | "ratios", 57 | "reds", 58 | "rgbs", 59 | "specifications", 60 | "specs", 61 | "wcag" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /test/alpha.css: -------------------------------------------------------------------------------- 1 | .a { 2 | background-color: rgba(0, 0, 0, 0.2); 3 | color: #fff; 4 | } 5 | 6 | .b { 7 | background-color: rgba(0, 0, 0, 0.5); 8 | color: #fff; 9 | } 10 | 11 | .c { 12 | background-color: rgba(0, 0, 0, 0.6); 13 | color: #fff; 14 | } 15 | 16 | .d { 17 | background-color: rgba(0, 0, 0, 0.8); 18 | color: #fff; 19 | } 20 | 21 | .e { 22 | background-color: #fff; 23 | color: rgba(0, 0, 0, 0.4); 24 | } 25 | 26 | .f { 27 | background-color: #fff; 28 | color: rgba(0, 0, 0, 0.5); 29 | } 30 | 31 | .g { 32 | background-color: #fff; 33 | color: rgba(0, 0, 0, 0.6); 34 | } 35 | 36 | .h { 37 | background-color: #fff; 38 | color: rgba(0, 0, 0, 0.7); 39 | } 40 | -------------------------------------------------------------------------------- /test/basic.css: -------------------------------------------------------------------------------- 1 | .a { 2 | background-color: #fff; 3 | color: #000; 4 | } 5 | 6 | .b { 7 | background-color: #ccc; 8 | color: #000; 9 | } 10 | 11 | .c { 12 | background-color: #777; 13 | color: #000; 14 | } 15 | 16 | .d { 17 | background-color: #444; 18 | color: #000; 19 | } 20 | 21 | .e { 22 | background-color: #111; 23 | color: #000; 24 | } 25 | 26 | .f { 27 | background: #fff url(kitten.jpg); 28 | color: #000; 29 | } 30 | 31 | .g { 32 | background: #777 url(kitten.jpg); 33 | color: #000; 34 | } 35 | 36 | .h { 37 | /* wcag-params: #eee */ 38 | color: #333; 39 | } 40 | -------------------------------------------------------------------------------- /test/basic.expect.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csstools/postcss-wcag-contrast/1eaf468e349d56521f14dbbf814aac920acb1d37/test/basic.expect.css -------------------------------------------------------------------------------- /test/comment.css: -------------------------------------------------------------------------------- 1 | .a { 2 | /* wcag-params: #fff */ 3 | color: #000; 4 | } 5 | 6 | .b { 7 | /* wcag-params: #777 */ 8 | color: #000; 9 | } 10 | 11 | .c { 12 | /* wcag-params: #444 */ 13 | color: #000; 14 | } 15 | 16 | .d { 17 | /* wcag-params: 18pt #fff */ 18 | color: #000; 19 | } 20 | 21 | .e { 22 | /* wcag-params: 18pt #777 */ 23 | color: #000; 24 | } 25 | 26 | .f { 27 | /* wcag-params: 18pt #444 */ 28 | color: #000; 29 | } 30 | 31 | .g { 32 | /* wcag-params: bold 14pt #fff */ 33 | color: #000; 34 | } 35 | 36 | .h { 37 | /* wcag-params: bold 14pt #777 */ 38 | color: #000; 39 | } 40 | 41 | .i { 42 | /* wcag-params: bold 14pt #444 */ 43 | color: #000; 44 | } 45 | 46 | .j { 47 | color: #000; 48 | } 49 | -------------------------------------------------------------------------------- /test/sized.css: -------------------------------------------------------------------------------- 1 | .a { 2 | background-color: #fff; 3 | color: #000; 4 | font-size: 18pt; 5 | } 6 | 7 | .b { 8 | background-color: #ccc; 9 | color: #000; 10 | font-size: 18pt; 11 | } 12 | 13 | .c { 14 | background-color: #777; 15 | color: #000; 16 | font-size: 18pt; 17 | } 18 | 19 | .d { 20 | background-color: #444; 21 | color: #000; 22 | font-size: 18pt; 23 | } 24 | 25 | .e { 26 | background-color: #111; 27 | color: #000; 28 | font-size: 18pt; 29 | } 30 | 31 | .f { 32 | background-color: #111; 33 | color: #000; 34 | font: 18pt sans-serif; 35 | } 36 | -------------------------------------------------------------------------------- /test/weighted.css: -------------------------------------------------------------------------------- 1 | .a { 2 | background-color: #fff; 3 | color: #000; 4 | font-size: 14pt; 5 | font-weight: bold; 6 | } 7 | 8 | .b { 9 | background-color: #ccc; 10 | color: #000; 11 | font-size: 14pt; 12 | font-weight: bold; 13 | } 14 | 15 | .c { 16 | background-color: #777; 17 | color: #000; 18 | font-size: 14pt; 19 | font-weight: bold; 20 | } 21 | 22 | .d { 23 | background-color: #444; 24 | color: #000; 25 | font-size: 14pt; 26 | font-weight: bold; 27 | } 28 | 29 | .e { 30 | background-color: #111; 31 | color: #000; 32 | font-size: 14pt; 33 | font-weight: bold; 34 | } 35 | 36 | .f { 37 | background-color: #777; 38 | color: #000; 39 | font: 18pt serif; 40 | } 41 | 42 | .g { 43 | background-color: #777; 44 | color: #000; 45 | font: bold 14pt serif; 46 | } 47 | 48 | .h { 49 | background-color: #777; 50 | color: #000; 51 | font: 14pt serif; 52 | } 53 | --------------------------------------------------------------------------------