├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── bin └── createColorTokens.js ├── color-tokens.js ├── package-lock.json ├── package.json └── sass ├── _color-token-contrast.md ├── _color-tokens.scss └── style.scss /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 5t3ph 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | sass 2 | color-tokens.js -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Stephanie Eckles 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://repository-images.githubusercontent.com/336923950/96da5980-6a67-11eb-8d4c-79f194eba4f8) 2 | 3 | # a11y-color-tokens 4 | 5 | > Generate accessible complementary text or UI colors as Sass variables and/or CSS custom properties from your base color tokens. 6 | 7 | ## Why do I need this? 8 | 9 | While many tools are available to _check_ contrast, but efficiently picking an accessible palette can be time-consuming and frustrating. As someone with way too many side projects, I'll say that color contrast is always something that slows down my workflow. In fact, I built this precisely to speed up my own process! 10 | 11 | Additionally, everyone benefits from documentation about color token contrast to ensure tokens are _used_ accessibly. 12 | 13 | `a11y-color-tokens` lets you focus on just selecting the base colors while taking care of generating contrast-safe complementary tones to ensure you meet [this important success criterion](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html). A unique feature of this project is that it scales the original color value for a more pleasing visual contrast vs only returning either white or black. (_Don't worry - you're able to override the contrast color if needed!_) 14 | 15 | > 💡 "Tokens" comes from the design system world terminology of "design tokens" which you can [learn more about from the original creator, Jina Anne](https://www.smashingmagazine.com/2019/11/smashing-podcast-episode-3/). 16 | 17 | ## What's in the box 18 | 19 | **Example output**: 20 | 21 | ```scss 22 | // `primary` name and value provided in your tokens 23 | $color-primary: rebeccapurple !default; 24 | // `on-primary` name and value generated by a11y-color-tokens 25 | // and guaranteed to have at least 4.5 contrast with `primary` 26 | $color-on-primary: #ceb3e9 !default; 27 | // Also includes a list based on your provided color names 28 | $base-color-tokens: ("primary", "secondary"); 29 | ``` 30 | 31 | The default options generate individual Sass variables, as well as a map of those variables and a mixin that contains the palette as CSS custom properties, ready for you to drop into `:root` or another location of your choice. 32 | 33 | Sass variables and the map include the `!default` flag as an additional way to extend, scale, and share your tokens. 34 | 35 | **[View the sample default output >](https://github.com/5t3ph/a11y-color-tokens/blob/main/sass/_color-tokens.scss)** 36 | 37 | Alternatively, pass `"css"` as the `tokenOutputFormat` to only output CSS custom properties within the `:root` selector. 38 | 39 | Additionally, [an optional Markdown document](https://github.com/5t3ph/a11y-color-tokens/blob/main/sass/_color-token-contrast.md) is generated with contrast cross-compatibility between all available color tokens. 40 | 41 | > Review an example of [using the generated Sass assets >](https://github.com/5t3ph/a11y-color-tokens/blob/main/sass/style.scss) 42 | 43 | ## Usage 44 | 45 | Install `a11y-color-tokens` into any project using: 46 | 47 | ```bash 48 | npm install a11y-color-tokens --save-dev 49 | ``` 50 | 51 | You can then add it to your scripts or call it directly from the command line, but first, you must prepare a color tokens file. 52 | 53 | ### Create Color Tokens File 54 | 55 | **Before the script will work**, you will need to prepare your color tokens as a module that exports the tokens array. 56 | 57 | The expected format is as follows: 58 | 59 | ```js 60 | // Example color-tokens.js 61 | module.exports = [ 62 | { 63 | /* 64 | * `name` - Required 65 | * Any string, will be used for color reference 66 | */ 67 | name: "primary", 68 | /* 69 | * `color` - Required 70 | * Any valid CSS color value 71 | */ 72 | color: "rgb(56, 84, 230)", 73 | /* 74 | * `onColor` - Optional 75 | * enum: undefined | "[css color value]" | false 76 | * 77 | * If undefined, will be generated as relative tone of `color` 78 | * that meets contrast according to `ratioKey` 79 | * 80 | * If a color value provided, will still be checked for contrast 81 | * and a warning comment added if it doesn't pass 82 | * 83 | * Set to `false` to omit generation 84 | */ 85 | /* 86 | * `ratioKey` - Optional 87 | * enum: undefined | "small" (default) | "large" 88 | * 89 | * Corresponds to mimimum contrast for either normal text ("small" = 4.5) 90 | * or large text/user interface components ("large" = 3) 91 | */ 92 | }, 93 | ]; 94 | ``` 95 | 96 | View [color-tokens.js](https://github.com/5t3ph/a11y-color-tokens/blob/main/color-tokens.js) in the package repo for more full example. 97 | 98 | ### Recommended Setup 99 | 100 | Add as a standalone script, and then call prior to your build and start commands to ensure tokens are always fresh. 101 | 102 | > At minimum, be sure to pass an existing `outputDirPath` (default: `"sass"`) and point `colorTokensPath` (default: `"color-tokens.js"`) to your tokens file. 103 | 104 | ```json 105 | "scripts": { 106 | "color-tokens": "a11y-color-tokens --outputDirPath='src/sass' --colorTokensPath='_theme/color-tokens.js'", 107 | "start": "npm-run-all color-tokens [your other scripts]", 108 | "build": "npm-run-all color-tokens [your other scripts]" 109 | }, 110 | ``` 111 | 112 | _**Sass processing is not included**, you must add that separately. This package is a great companion to my [11ty-sass-skeleton template](https://github.com/5t3ph/11ty-sass-skeleton) which is a barebones Eleventy static site_. 113 | 114 | ## Config Options 115 | 116 | | Option | Type | Default | 117 | | ----------------------- | ----------------------- | ---------------------- | 118 | | outputDirPath | string | "sass" | 119 | | outputFilename | string | "\_color-tokens.scss" | 120 | | colorTokensPath | string | "color-tokens.js" | 121 | | tokenOutputFormat | enum: "sass" \| "css" | "sass" | 122 | | sassOutputName | string | "color-tokens" | 123 | | tokenPrefix | enum: string \| boolean | "color-" | 124 | | compatibilityDocs | boolean | true | 125 | | compatibilityDocsPath | string | {outputDirPath} | 126 | | compatibilityDocsTitle | string | "Color Token Contrast" | 127 | | includeCustomProperties | boolean | true | 128 | | customPropertiesFormat | enum: "mixin" \| "root" | "mixin" | 129 | 130 | > To set a boolean option to `false`, format the option as `--no-[optionName]` 131 | 132 | ## Config Examples 133 | 134 | ### Vanilla CSS output of custom properties 135 | 136 | As noted in the intro, the default output is Sass based. 137 | 138 | Flip this to output all generated tokens as CSS custom properties within `:root` with the following: 139 | 140 | ```bash 141 | a11y-color-tokens --tokenOutputFormat='css' --outputFilename='theme-colors.css' 142 | ``` 143 | 144 | > For the CSS-only output, you will need to update `outputFilename` since the default creates this output as a Sass (`.scss`) file. 145 | 146 | ### Direct `:root` Sass output of custom properties 147 | 148 | The default creates a `mixin` containing the CSS custom properties version of the tokens. If you'd rather output them in `:root` directly, set the following: 149 | 150 | ```bash 151 | a11y-color-tokens --customPropertiesFormat='root' 152 | ``` 153 | 154 | ### Update Sass map and mixin name 155 | 156 | This is handled by updating the following: 157 | 158 | ```bash 159 | a11y-color-tokens --sassOutputName='colors' 160 | ``` 161 | 162 | ### Update or remove the generated token prefix 163 | 164 | Change the prefix of `color-` by setting a new value, or use `--no-tokenPrefix` to remove token prefixing. 165 | 166 | ```bash 167 | a11y-color-tokens --tokenPrefix='theme-' 168 | ``` 169 | 170 | ### Prevent CSS custom properties output 171 | 172 | This is handled with `includeCustomProperties` and can be removed with: 173 | 174 | ```bash 175 | a11y-color-tokens --no-includeCustomProperties 176 | ``` 177 | 178 | ### Remove the `_color-token-contrast.md` documentation 179 | 180 | This is handled with `compatibilityDocs` and can be removed with: 181 | 182 | ```bash 183 | a11y-color-tokens --no-compatibilityDocs 184 | ``` 185 | 186 | ### Change output location of `_color-token-contrast.md` 187 | 188 | The default places the docs in the same directory defined for `outputDirPath`. 189 | 190 | To change, supply a new file path (the `_` prefix will be removed from the Markdown filename as well): 191 | 192 | ```bash 193 | a11y-color-tokens --compatibilityDocsPath='docs' 194 | ``` 195 | 196 | ## Colophon and Credits 197 | 198 | Hi! I'm [Stephanie Eckles - @5t3ph](https://twitter.com/5t3ph) and I've been a front-end focused developer for over a decade. [Check out more of my projects](https://thinkdobecreate.com) including in-depth tutorials to help you upgrade your CSS skills on [ModernCSS.dev](https://moderncss.dev), and my [egghead video lessons](https://5t3ph.dev/egghead) on all kinds of front-end topics. 199 | 200 | `a11y-color-tokens` relies on the following packages: 201 | 202 | - [color](https://www.npmjs.com/package/color) - for contrast checking and color model evaluation 203 | - [a11ycolor](https://www.npmjs.com/package/a11ycolor) - for finding the nearest contrast-safe match 204 | 205 | > If you've found this project useful, I'd appreciate ☕️ [a coffee to keep me coding](https://www.buymeacoffee.com/moderncss)! 206 | -------------------------------------------------------------------------------- /bin/createColorTokens.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const a11yColor = require("a11ycolor"); 6 | const Color = require("color"); 7 | const options = require("yargs-parser")(process.argv.slice(2)); 8 | 9 | const cssOutputFilename = "color-tokens.css"; 10 | const defaults = { 11 | outputDirPath: "sass", 12 | outputFilename: "_color-tokens.scss", 13 | colorTokensPath: "color-tokens.js", 14 | tokenOutputFormat: "sass", // "css" 15 | sassOutputName: "color-tokens", 16 | tokenPrefix: "color-", 17 | compatibilityDocs: true, 18 | compatibilityDocsPath: "outputDirPath", 19 | compatibilityDocsTitle: "Color Token Contrast", 20 | includeCustomProperties: true, 21 | customPropertiesFormat: "mixin", // || "root" 22 | }; 23 | 24 | const { 25 | outputDirPath, 26 | outputFilename, 27 | colorTokensPath, 28 | tokenOutputFormat, 29 | sassOutputName, 30 | tokenPrefix, 31 | compatibilityDocs, 32 | compatibilityDocsPath, 33 | compatibilityDocsTitle, 34 | includeCustomProperties, 35 | customPropertiesFormat, 36 | } = { 37 | ...defaults, 38 | ...options, 39 | }; 40 | 41 | let colorTokensFile = path.resolve(__dirname, fs.realpathSync(colorTokensPath)); 42 | if (!fs.existsSync(colorTokensFile)) { 43 | console.log("Invalid colorTokensFile provided"); 44 | process.exit(1); 45 | } 46 | const themeColors = require(colorTokensFile); 47 | 48 | const checkContrast = (foreground, background = "#fff") => { 49 | const colorValue = Color(foreground); 50 | // Determine contrast against white as a baseline 51 | return colorValue.contrast(Color(background)); 52 | }; 53 | 54 | // @link https://css-tricks.com/snippets/javascript/lighten-darken-color/ 55 | const onColorTone = (color, amount) => { 56 | let usePound = false; 57 | 58 | if (color[0] == "#") { 59 | color = color.slice(1); 60 | usePound = true; 61 | } 62 | 63 | const num = parseInt(color, 16); 64 | 65 | let r = (num >> 16) + amount; 66 | 67 | if (r > 255) r = 255; 68 | else if (r < 0) r = 0; 69 | 70 | let b = ((num >> 8) & 0x00ff) + amount; 71 | 72 | if (b > 255) b = 255; 73 | else if (b < 0) b = 0; 74 | 75 | let g = (num & 0x0000ff) + amount; 76 | 77 | if (g > 255) g = 255; 78 | else if (g < 0) g = 0; 79 | 80 | return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); 81 | }; 82 | 83 | const onColorContrast = (color, ratioKey) => { 84 | const contrast = checkContrast(color); 85 | const contrastRatio = ratioKey === "large" ? 3 : 4.5; 86 | // Alter the color darker or lighter 87 | let onColor = 88 | contrast >= contrastRatio 89 | ? onColorTone(color, 60) 90 | : onColorTone(color, -60); 91 | // Make sure `onColor` value meets ratioKey threshold 92 | return a11yColor(onColor, color, ($ratioKey = ratioKey)); 93 | }; 94 | 95 | const colorOutput = (colors, prefix, eol, join, type) => { 96 | return colors 97 | .map(({ name, color, warn }) => { 98 | const warning = warn 99 | ? `/* 🚫 Contrast fails against ${name.split("-")[2]} */\n` 100 | : ""; 101 | let colorValue = type === "map" ? `$${name}` : color; 102 | colorValue = type === "mixin" ? `#{$${name}}` : colorValue; 103 | return `${warning}${prefix}${name}${ 104 | prefix == '"' ? '"' : "" 105 | }: ${colorValue}${eol}`; 106 | }) 107 | .join(join); 108 | }; 109 | 110 | const generateContrastDocs = (colors) => { 111 | let compatibilityResults = `---\ntitle: ${compatibilityDocsTitle}\n---\n\n`; 112 | compatibilityResults += 113 | "> The following are contrast safe combinations as calculated for _normal_ text based on WCAG AA 4.5\n"; 114 | colors.map(({ name, color }) => { 115 | const rest = colors.filter((c) => c.color !== color); 116 | const restResults = rest 117 | .map((oc) => { 118 | const passing = checkContrast(color, oc.color) > 4.5; 119 | return passing ? oc.name : null; 120 | }) 121 | .filter((c) => c); 122 | compatibilityResults += `\n## ${name}\n - ${ 123 | restResults.length 124 | ? `\`${restResults.join("`\n - `")}\`` 125 | : "No safe options" 126 | }\n`; 127 | }); 128 | 129 | return compatibilityResults; 130 | }; 131 | 132 | (async () => { 133 | let colors = []; 134 | const prefix = tokenPrefix || ""; 135 | 136 | themeColors.map(({ name, color, onColor, ratioKey = "small" }) => { 137 | colors.push({ name: `${prefix}${name}`, color }); 138 | 139 | const contrastRatio = ratioKey === "large" ? 3 : 4.5; 140 | const isHSL = Color(color).object().h; 141 | const alphaRE = RegExp("^[a-zA-Z]+$", "i"); 142 | const isNamedColor = alphaRE.test(color); 143 | 144 | let onColorValue = onColor; 145 | if (onColor !== false) { 146 | let warn = false; 147 | if (!onColorValue) { 148 | onColorValue = onColorContrast(Color(color).hex(), ratioKey); 149 | // Convert back to RGB 150 | onColorValue = 151 | !isHSL && !color.includes("#") && !isNamedColor 152 | ? Color(onColorValue).rgb() 153 | : onColorValue; 154 | // Convert back to HSL 155 | onColorValue = isHSL ? Color(onColorValue).hsl() : onColorValue; 156 | } else { 157 | warn = 158 | checkContrast(onColorValue, color) >= contrastRatio ? false : true; 159 | } 160 | colors.push({ 161 | name: `${prefix}on-${name}`, 162 | color: onColorValue, 163 | warn, 164 | }); 165 | } 166 | }); 167 | 168 | let tokenOutput = ""; 169 | 170 | if (tokenOutputFormat === "sass") { 171 | tokenOutput = colorOutput(colors, "$", " !default;", "\n"); 172 | 173 | tokenOutput += `\n\n$base-${sassOutputName}: (${themeColors 174 | .map(({ name }) => `"${name}"`) 175 | .join(", ")});`; 176 | 177 | tokenOutput += `\n\n$${sassOutputName}: ( 178 | ${colorOutput(colors, '"', "", ",\n ", "map")} 179 | ) !default;`; 180 | } 181 | 182 | let themeColorOutput = `/* 🛑 STOP!\n Do not change this file directly.\n Modify colors in ${colorTokensPath}\n */`; 183 | 184 | themeColorOutput += `\n\n${tokenOutput}`; 185 | 186 | if (includeCustomProperties || tokenOutputFormat === "css") { 187 | if (tokenOutputFormat === "css" || customPropertiesFormat === "root") { 188 | themeColorOutput += `\n\n:root {\n ${colorOutput( 189 | colors, 190 | "--", 191 | ";", 192 | "\n ", 193 | tokenOutputFormat === "sass" && "mixin" 194 | )}\n}`; 195 | } else { 196 | themeColorOutput += `\n\n@mixin ${sassOutputName}() {\n ${colorOutput( 197 | colors, 198 | "--", 199 | ";", 200 | "\n ", 201 | "mixin" 202 | )}\n}`; 203 | } 204 | } 205 | 206 | const filename = 207 | tokenOutputFormat === "sass" ? outputFilename : cssOutputFilename; 208 | let themeColorsFilePath = path.resolve( 209 | __dirname, 210 | fs.realpathSync(outputDirPath) 211 | ); 212 | if (!fs.existsSync(themeColorsFilePath)) { 213 | console.log("Invalid outputDirPath provided"); 214 | process.exit(1); 215 | } 216 | fs.writeFileSync(`${themeColorsFilePath}/${filename}`, themeColorOutput, { 217 | flag: "w", 218 | }); 219 | 220 | if (compatibilityDocs) { 221 | const docs = generateContrastDocs(colors); 222 | let docsPath = `${themeColorsFilePath}/_color-token-contrast.md`; 223 | 224 | if (compatibilityDocsPath !== "outputDirPath") { 225 | docsPath = `${compatibilityDocsPath}/color-token-contrast.md`; 226 | } 227 | 228 | fs.writeFileSync(docsPath, docs, { 229 | flag: "w", 230 | }); 231 | } 232 | })(); 233 | -------------------------------------------------------------------------------- /color-tokens.js: -------------------------------------------------------------------------------- 1 | // Example of expected format 2 | module.exports = [ 3 | { 4 | /* 5 | * `name` - Required 6 | * Any string, will be used for color reference 7 | */ 8 | name: "primary", 9 | /* 10 | * `color` - Required 11 | * Any valid CSS color value 12 | */ 13 | color: "rebeccapurple", 14 | /* 15 | * `onColor` - Optional 16 | * enum: undefined | "[css color value]" | false 17 | * 18 | * If undefined, will be generated as relative tone of `color` 19 | * that meets contrast according to `ratioKey` 20 | * 21 | * If a color value provided, will still be checked for contrast 22 | * and a warning comment added if it doesn't pass 23 | * 24 | * Set to `false` to omit generation 25 | */ 26 | /* 27 | * `ratioKey` - Optional 28 | * enum: undefined | "small" (default) | "large" 29 | * 30 | * Corresponds to mimimum contrast for either normal text ("small" = 4.5) 31 | * or large text/user interface components ("large" = 3) 32 | */ 33 | }, 34 | { 35 | name: "secondary", 36 | color: "rgb(95, 165, 26)", 37 | }, 38 | { 39 | name: "tertiary", 40 | color: "hsl(245, 70%, 30%)", 41 | }, 42 | { 43 | name: "surface", 44 | color: "#f9f9f9", 45 | onColor: "#494848", 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a11y-color-tokens", 3 | "version": "0.7.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "a11y-color-tokens", 9 | "version": "0.7.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "a11ycolor": "^2.0.9", 13 | "color": "^4.2.3", 14 | "yargs-parser": "^21.1.1" 15 | }, 16 | "bin": { 17 | "a11y-color-tokens": "bin/createColorTokens.js" 18 | } 19 | }, 20 | "node_modules/a11ycolor": { 21 | "version": "2.0.9", 22 | "resolved": "https://registry.npmjs.org/a11ycolor/-/a11ycolor-2.0.9.tgz", 23 | "integrity": "sha512-zSuRuunDy/z76czO2ygb7GEk1c9uiTzeFczLVL1HEymOKkH3eqNNFBEEVYtJvbc7ChFNCsvIX+2PfHf2PWAtNg==", 24 | "dependencies": { 25 | "color": "^3.1.2" 26 | } 27 | }, 28 | "node_modules/a11ycolor/node_modules/color": { 29 | "version": "3.2.1", 30 | "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", 31 | "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", 32 | "dependencies": { 33 | "color-convert": "^1.9.3", 34 | "color-string": "^1.6.0" 35 | } 36 | }, 37 | "node_modules/a11ycolor/node_modules/color-convert": { 38 | "version": "1.9.3", 39 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 40 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 41 | "dependencies": { 42 | "color-name": "1.1.3" 43 | } 44 | }, 45 | "node_modules/a11ycolor/node_modules/color-name": { 46 | "version": "1.1.3", 47 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 48 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" 49 | }, 50 | "node_modules/color": { 51 | "version": "4.2.3", 52 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 53 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 54 | "dependencies": { 55 | "color-convert": "^2.0.1", 56 | "color-string": "^1.9.0" 57 | }, 58 | "engines": { 59 | "node": ">=12.5.0" 60 | } 61 | }, 62 | "node_modules/color-convert": { 63 | "version": "2.0.1", 64 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 65 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 66 | "dependencies": { 67 | "color-name": "~1.1.4" 68 | }, 69 | "engines": { 70 | "node": ">=7.0.0" 71 | } 72 | }, 73 | "node_modules/color-name": { 74 | "version": "1.1.4", 75 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 76 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 77 | }, 78 | "node_modules/color-string": { 79 | "version": "1.9.1", 80 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 81 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 82 | "dependencies": { 83 | "color-name": "^1.0.0", 84 | "simple-swizzle": "^0.2.2" 85 | } 86 | }, 87 | "node_modules/is-arrayish": { 88 | "version": "0.3.2", 89 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 90 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 91 | }, 92 | "node_modules/simple-swizzle": { 93 | "version": "0.2.2", 94 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 95 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 96 | "dependencies": { 97 | "is-arrayish": "^0.3.1" 98 | } 99 | }, 100 | "node_modules/yargs-parser": { 101 | "version": "21.1.1", 102 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 103 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 104 | "engines": { 105 | "node": ">=12" 106 | } 107 | } 108 | }, 109 | "dependencies": { 110 | "a11ycolor": { 111 | "version": "2.0.9", 112 | "resolved": "https://registry.npmjs.org/a11ycolor/-/a11ycolor-2.0.9.tgz", 113 | "integrity": "sha512-zSuRuunDy/z76czO2ygb7GEk1c9uiTzeFczLVL1HEymOKkH3eqNNFBEEVYtJvbc7ChFNCsvIX+2PfHf2PWAtNg==", 114 | "requires": { 115 | "color": "^3.1.2" 116 | }, 117 | "dependencies": { 118 | "color": { 119 | "version": "3.2.1", 120 | "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", 121 | "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", 122 | "requires": { 123 | "color-convert": "^1.9.3", 124 | "color-string": "^1.6.0" 125 | } 126 | }, 127 | "color-convert": { 128 | "version": "1.9.3", 129 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 130 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 131 | "requires": { 132 | "color-name": "1.1.3" 133 | } 134 | }, 135 | "color-name": { 136 | "version": "1.1.3", 137 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 138 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" 139 | } 140 | } 141 | }, 142 | "color": { 143 | "version": "4.2.3", 144 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 145 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 146 | "requires": { 147 | "color-convert": "^2.0.1", 148 | "color-string": "^1.9.0" 149 | } 150 | }, 151 | "color-convert": { 152 | "version": "2.0.1", 153 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 154 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 155 | "requires": { 156 | "color-name": "~1.1.4" 157 | } 158 | }, 159 | "color-name": { 160 | "version": "1.1.4", 161 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 162 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 163 | }, 164 | "color-string": { 165 | "version": "1.9.1", 166 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 167 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 168 | "requires": { 169 | "color-name": "^1.0.0", 170 | "simple-swizzle": "^0.2.2" 171 | } 172 | }, 173 | "is-arrayish": { 174 | "version": "0.3.2", 175 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 176 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 177 | }, 178 | "simple-swizzle": { 179 | "version": "0.2.2", 180 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 181 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 182 | "requires": { 183 | "is-arrayish": "^0.3.1" 184 | } 185 | }, 186 | "yargs-parser": { 187 | "version": "21.1.1", 188 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 189 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a11y-color-tokens", 3 | "version": "0.7.0", 4 | "description": "Generate accessible complementary text or UI colors as Sass variables and/or CSS custom properties from your base color tokens.", 5 | "main": "bin/createColorTokens.js", 6 | "scripts": { 7 | "bump": "npm --no-git-tag-version version", 8 | "tokens": "node bin/createColorTokens.js" 9 | }, 10 | "bin": { 11 | "a11y-color-tokens": "bin/createColorTokens.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/5t3ph/a11y-color-tokens.git" 16 | }, 17 | "keywords": [ 18 | "accessibility", 19 | "a11y", 20 | "color", 21 | "css", 22 | "sass", 23 | "scss", 24 | "design system", 25 | "design tokens", 26 | "tokens" 27 | ], 28 | "author": { 29 | "name": "5t3ph", 30 | "url": "https://thinkdobecreate.com" 31 | }, 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/5t3ph/a11y-color-tokens/issues" 35 | }, 36 | "homepage": "https://github.com/5t3ph/a11y-color-tokens#readme", 37 | "dependencies": { 38 | "a11ycolor": "^2.0.9", 39 | "color": "^4.2.3", 40 | "yargs-parser": "^21.1.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sass/_color-token-contrast.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Color Token Contrast 3 | --- 4 | 5 | > The following are contrast safe combinations as calculated for _normal_ text based on WCAG AA 4.5 6 | 7 | ## color-primary 8 | - `color-on-primary` 9 | - `color-surface` 10 | 11 | ## color-on-primary 12 | - `color-primary` 13 | - `color-on-secondary` 14 | - `color-tertiary` 15 | - `color-on-surface` 16 | 17 | ## color-secondary 18 | - `color-on-secondary` 19 | - `color-tertiary` 20 | 21 | ## color-on-secondary 22 | - `color-on-primary` 23 | - `color-secondary` 24 | - `color-on-tertiary` 25 | - `color-surface` 26 | 27 | ## color-tertiary 28 | - `color-on-primary` 29 | - `color-secondary` 30 | - `color-on-tertiary` 31 | - `color-surface` 32 | 33 | ## color-on-tertiary 34 | - `color-on-secondary` 35 | - `color-tertiary` 36 | 37 | ## color-surface 38 | - `color-primary` 39 | - `color-on-secondary` 40 | - `color-tertiary` 41 | - `color-on-surface` 42 | 43 | ## color-on-surface 44 | - `color-on-primary` 45 | - `color-surface` 46 | -------------------------------------------------------------------------------- /sass/_color-tokens.scss: -------------------------------------------------------------------------------- 1 | /* 🛑 STOP! 2 | Do not change this file directly. 3 | Modify colors in color-tokens.js 4 | */ 5 | 6 | $color-primary: rebeccapurple !default; 7 | $color-on-primary: #CEB3E9 !default; 8 | $color-secondary: rgb(95, 165, 26) !default; 9 | $color-on-secondary: rgb(17, 52, 0) !default; 10 | $color-tertiary: hsl(245, 70%, 30%) !default; 11 | $color-on-tertiary: hsl(245, 45%, 68.6%) !default; 12 | $color-surface: #f9f9f9 !default; 13 | $color-on-surface: #494848 !default; 14 | 15 | $base-color-tokens: ("primary", "secondary", "tertiary", "surface"); 16 | 17 | $color-tokens: ( 18 | "color-primary": $color-primary, 19 | "color-on-primary": $color-on-primary, 20 | "color-secondary": $color-secondary, 21 | "color-on-secondary": $color-on-secondary, 22 | "color-tertiary": $color-tertiary, 23 | "color-on-tertiary": $color-on-tertiary, 24 | "color-surface": $color-surface, 25 | "color-on-surface": $color-on-surface 26 | ) !default; 27 | 28 | @mixin color-tokens() { 29 | --color-primary: #{$color-primary}; 30 | --color-on-primary: #{$color-on-primary}; 31 | --color-secondary: #{$color-secondary}; 32 | --color-on-secondary: #{$color-on-secondary}; 33 | --color-tertiary: #{$color-tertiary}; 34 | --color-on-tertiary: #{$color-on-tertiary}; 35 | --color-surface: #{$color-surface}; 36 | --color-on-surface: #{$color-on-surface}; 37 | } -------------------------------------------------------------------------------- /sass/style.scss: -------------------------------------------------------------------------------- 1 | // Import the Sass file generated by a11y-color-tokens 2 | @use "color-tokens" as *; 3 | 4 | :root { 5 | // Include the generated CSS custom properties 6 | @include color-tokens(); 7 | } 8 | 9 | body { 10 | // Use the CSS custom properties 11 | background-color: var(--color-surface); 12 | color: var(--color-on-surface); 13 | } 14 | 15 | // Loop through the provided $base-color-tokens to generate classes 16 | @each $color in $base-color-tokens { 17 | .theme-#{$color} { 18 | background-color: var(--color-#{$color}); 19 | color: var(--color-on-#{$color}); 20 | } 21 | } 22 | --------------------------------------------------------------------------------