├── .editorconfig ├── .eslintrc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── index.d.ts ├── index.js └── utils.js ├── tests ├── helpers.js ├── index.test.js └── utils.test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "extends": ["airbnb-base", "prettier"], 6 | "plugins": ["prettier"], 7 | "rules": { 8 | "import/no-extraneous-dependencies": "off", 9 | "prettier/prettier": ["error"], 10 | "func-names": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: yarn install --frozen-lockfile 27 | - run: yarn test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | yarn-error.log 5 | /coverage/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | 4 | CHANGELOG.md 5 | 6 | yarn.lock 7 | yarn-error.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.1.2](https://github.com/enjidev/tailwindcss-accent/compare/v2.1.1...v2.1.2) (2023-01-05) 6 | 7 | ### [2.1.1](https://github.com/enjidev/tailwindcss-accent/compare/v2.1.0...v2.1.1) (2022-12-07) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * root color specificity ([674413f](https://github.com/enjidev/tailwindcss-accent/commit/674413f699b1391a4fa164051c6205b4a95be7ab)) 13 | 14 | ## [2.1.0](https://github.com/enjidev/tailwindcss-accent/compare/v2.0.0...v2.1.0) (2022-12-03) 15 | 16 | 17 | ### Features 18 | 19 | * add cssVarsPrefix option ([bfa5fec](https://github.com/enjidev/tailwindcss-accent/commit/bfa5fec2eedf4306fc161e2d84591f1c38d3dbe2)) 20 | 21 | ## [2.0.0](https://github.com/enjidev/tailwindcss-accent/compare/v1.2.0...v2.0.0) (2022-12-03) 22 | 23 | 24 | ### ⚠ BREAKING CHANGES 25 | 26 | * set colors option as required 27 | 28 | ### Features 29 | 30 | * remove lodash dependency ([efb443d](https://github.com/enjidev/tailwindcss-accent/commit/efb443d4afca9bcdb6634e8354188cec88892638)) 31 | 32 | 33 | * set colors option as required ([ef6b983](https://github.com/enjidev/tailwindcss-accent/commit/ef6b98397099100bdc6e3e520eda33d2075cae3b)) 34 | 35 | ## [1.2.0](https://github.com/enjidev/tailwindcss-accent/compare/v1.1.0...v1.2.0) (2022-12-03) 36 | 37 | 38 | ### Features 39 | 40 | * add root option ([b20d472](https://github.com/enjidev/tailwindcss-accent/commit/b20d4722209704cad9d9c640aefe7e5acc961a99)) 41 | * support for Tailwind CSS v2 and v3 ([7e39851](https://github.com/enjidev/tailwindcss-accent/commit/7e39851b7d2ddca2de747f4089b15f0f77db4ffe)) 42 | 43 | ## 1.1.0 (2022-12-02) 44 | 45 | 46 | ### Features 47 | 48 | * add basic plugin functionality ([281fdfd](https://github.com/enjidev/tailwindcss-accent/commit/281fdfd7c007b1be193083826354baee6ce7c849)) 49 | * add plugin types ([a7785f3](https://github.com/enjidev/tailwindcss-accent/commit/a7785f30bfffe9d70f43f06c5e94ac11b66d7193)) 50 | * initial commit ([46e9fd4](https://github.com/enjidev/tailwindcss-accent/commit/46e9fd4f517381ac7cd41058f65c6c1d5bb53eeb)) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * typo on extend keyword ([fe245fe](https://github.com/enjidev/tailwindcss-accent/commit/fe245fe3a2b03e727eda4b59c5939e76bd960c27)) 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) enjidev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tailwindcss-accent 2 | 3 | A Tailwind CSS plugin that provides `accent` color utilities using CSS custom properties based on the Tailwind CSS default color palette. 4 | 5 | --- 6 | 7 | ## Documentation 8 | 9 | For full documentation, visit [enji.dev/docs/tailwindcss-accent](https://www.enji.dev/docs/tailwindcss-accent). 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-accent", 3 | "version": "2.1.2", 4 | "description": "Add dynamic accent color to your Tailwind CSS project.", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "files": [ 8 | "src/index.js", 9 | "src/index.d.ts", 10 | "src/utils.js" 11 | ], 12 | "scripts": { 13 | "lint": "eslint .", 14 | "lint:fix": "eslint . --fix", 15 | "format": "prettier --write --loglevel=error .", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "release": "standard-version", 19 | "postrelease": "git push --follow-tags origin main && npm publish" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "npm run lint:fix && npm run format && npm run test" 24 | } 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/enjidev/tailwindcss-accent.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/enjidev/tailwindcss-accent/issues" 32 | }, 33 | "homepage": "https://github.com/enjidev/tailwindcss-accent#readme", 34 | "keywords": [ 35 | "tailwind-css-plugin", 36 | "tailwindcss", 37 | "plugin", 38 | "accent-color", 39 | "theming", 40 | "theme" 41 | ], 42 | "author": "enjidev", 43 | "license": "MIT", 44 | "devDependencies": { 45 | "eslint": "^7.2.0", 46 | "eslint-config-airbnb-base": "^14.2.1", 47 | "eslint-config-prettier": "^6.15.0", 48 | "eslint-plugin-import": "^2.22.1", 49 | "eslint-plugin-jest": "^24.1.3", 50 | "eslint-plugin-prettier": "^3.1.4", 51 | "husky": "^4.3.0", 52 | "jest": "^26.6.3", 53 | "lint-staged": "^10.5.2", 54 | "lodash": "^4.17.20", 55 | "postcss": "^8.1.9", 56 | "prettier": "^2.2.0", 57 | "standard-version": "^9.1.0", 58 | "tailwindcss": "^3.0.0", 59 | "tailwindcss-v2": "npm:tailwindcss@^2.0.0" 60 | }, 61 | "dependencies": { 62 | "color-convert": "^2.0.1" 63 | }, 64 | "peerDependencies": { 65 | "tailwindcss": ">=2.0.0 || >=3.0.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultColors } from 'tailwindcss/types/generated/colors'; 2 | 3 | type Colors = keyof Omit< 4 | DefaultColors, 5 | 'inherit' | 'current' | 'transparent' | 'black' | 'white' 6 | >; 7 | 8 | declare function plugin(options: { 9 | /** 10 | * Include specific color(s). 11 | */ 12 | colors: Array; 13 | 14 | /** 15 | * Set color from colors option as :root accent color. 16 | */ 17 | root?: T; 18 | 19 | /** 20 | * Set prefix for css variables name. Default to 'tw-ta'. 21 | */ 22 | cssVarsPrefix?: string; 23 | }): { 24 | handler: () => void; 25 | }; 26 | 27 | declare namespace plugin { 28 | const __isOptionsFunction: true; 29 | } 30 | 31 | export = plugin; 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | const colors = require('tailwindcss/colors'); 3 | 4 | const { hexToRgb, withOpacityValue } = require('./utils'); 5 | 6 | module.exports = plugin.withOptions( 7 | (options = {}) => { 8 | // Early return if the colors option isn't specified. 9 | if (!options.colors && !Array.isArray(options.colors)) return () => {}; 10 | 11 | const cssVarsPrefix = options.cssVarsPrefix 12 | ? options.cssVarsPrefix 13 | : 'tw-ta'; 14 | 15 | return ({ addBase }) => { 16 | const rootStyles = {}; 17 | const baseStyles = {}; 18 | 19 | const PICK_COLORS = options.colors; 20 | const OMIT_COLORS = [ 21 | 'black', 22 | 'white', 23 | 'inherit', 24 | 'current', 25 | 'transparent', 26 | ]; 27 | 28 | Object.keys(colors).forEach((name) => { 29 | // Omit unused colors and pick selected colors. 30 | if (!OMIT_COLORS.includes(name) && PICK_COLORS.includes(name)) { 31 | const colorShades = colors[name]; 32 | 33 | // Generate CSS Custom Properties for the current color. 34 | const styles = {}; 35 | Object.keys(colorShades).forEach((shade) => { 36 | const cssVar = `--${cssVarsPrefix}-accent-${shade}`; 37 | styles[cssVar] = hexToRgb(colorShades[shade]); 38 | }); 39 | 40 | // Add color to root or base styles. 41 | if (options.root === name) { 42 | rootStyles[`:root, [data-accent='${name}']`] = styles; 43 | } else { 44 | baseStyles[`[data-accent='${name}']`] = styles; 45 | } 46 | } 47 | }); 48 | 49 | // Register new base styles. :root color comes first for CSS specificity. 50 | addBase(rootStyles); 51 | addBase(baseStyles); 52 | }; 53 | }, 54 | (options = {}) => { 55 | const cssVarsPrefix = options.cssVarsPrefix 56 | ? options.cssVarsPrefix 57 | : 'tw-ta'; 58 | 59 | return { 60 | theme: { 61 | extend: { 62 | colors: { 63 | accent: { 64 | 50: withOpacityValue(`--${cssVarsPrefix}-accent-50`), 65 | 100: withOpacityValue(`--${cssVarsPrefix}-accent-100`), 66 | 200: withOpacityValue(`--${cssVarsPrefix}-accent-200`), 67 | 300: withOpacityValue(`--${cssVarsPrefix}-accent-300`), 68 | 400: withOpacityValue(`--${cssVarsPrefix}-accent-400`), 69 | 500: withOpacityValue(`--${cssVarsPrefix}-accent-500`), 70 | 600: withOpacityValue(`--${cssVarsPrefix}-accent-600`), 71 | 700: withOpacityValue(`--${cssVarsPrefix}-accent-700`), 72 | 800: withOpacityValue(`--${cssVarsPrefix}-accent-800`), 73 | 900: withOpacityValue(`--${cssVarsPrefix}-accent-900`), 74 | }, 75 | }, 76 | }, 77 | }, 78 | }; 79 | } 80 | ); 81 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const convert = require('color-convert'); 2 | 3 | module.exports.hexToRgb = (hex) => { 4 | return convert.hex.rgb(hex).join(' '); 5 | }; 6 | 7 | module.exports.withOpacityValue = (variable) => { 8 | return ({ opacityValue }) => { 9 | if (opacityValue === undefined) { 10 | return `rgb(var(${variable}))`; 11 | } 12 | return `rgb(var(${variable}) / ${opacityValue})`; 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const tailwindcss = require('tailwindcss'); 3 | const tailwindcssV2 = require('tailwindcss-v2'); 4 | const customPlugin = require('../src/index'); 5 | 6 | module.exports.generatePluginCss = (options) => { 7 | const config = { 8 | corePlugins: false, 9 | plugins: [customPlugin(options)], 10 | }; 11 | 12 | return postcss(tailwindcss(config)) 13 | .process('@tailwind base', { 14 | from: undefined, 15 | }) 16 | .then(({ css }) => css); 17 | }; 18 | 19 | module.exports.generatePluginCssV2 = (options) => { 20 | const config = { 21 | corePlugins: false, 22 | plugins: [customPlugin(options)], 23 | }; 24 | 25 | return postcss(tailwindcssV2(config)) 26 | .process('@tailwind base', { 27 | from: undefined, 28 | }) 29 | .then(({ css }) => css); 30 | }; 31 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | const { generatePluginCss, generatePluginCssV2 } = require('./helpers'); 2 | 3 | describe('with tailwindcss v3', () => { 4 | it('returns nothing if colors option is not specified.', () => { 5 | return generatePluginCss().then((css) => { 6 | expect(css).toEqual(''); 7 | }); 8 | }); 9 | 10 | it('returns nothing if colors option is empty string.', () => { 11 | return generatePluginCss({ colors: '' }).then((css) => { 12 | expect(css).toEqual(''); 13 | }); 14 | }); 15 | 16 | it('returns nothing if colors option is empty array.', () => { 17 | return generatePluginCss({ colors: [] }).then((css) => { 18 | expect(css).toEqual(''); 19 | }); 20 | }); 21 | 22 | it('returns nothing if colors option is array with not available colors.', () => { 23 | return generatePluginCss({ colors: ['maroon'] }).then((css) => { 24 | expect(css).toEqual(''); 25 | }); 26 | }); 27 | 28 | it('returns nothing if root option is specified and colors option is not specified.', () => { 29 | return generatePluginCss({ root: 'sky' }).then((css) => { 30 | expect(css).toEqual(''); 31 | }); 32 | }); 33 | 34 | it('correctly generates CSS custom properties.', () => { 35 | return generatePluginCss({ colors: ['rose'] }).then((css) => { 36 | expect(css).toContain(`--tw-ta-accent-500`); 37 | }); 38 | }); 39 | 40 | it('correctly generates customized CSS custom properties.', () => { 41 | return generatePluginCss({ colors: ['rose'], cssVarsPrefix: 'color' }).then( 42 | (css) => { 43 | expect(css).toContain(`--color-accent-500`); 44 | } 45 | ); 46 | }); 47 | 48 | it('correctly generates selected base style selectors.', () => { 49 | return generatePluginCss({ colors: ['rose'] }).then((css) => { 50 | expect(css).toContain(`[data-accent='rose']`); 51 | expect(css).not.toContain(`[data-accent='sky']`); 52 | }); 53 | }); 54 | 55 | it('correctly generates selected base style selectors with root.', () => { 56 | return generatePluginCss({ colors: ['sky', 'rose'], root: 'sky' }).then( 57 | (css) => { 58 | expect(css).toContain(`:root, [data-accent='sky']`); 59 | expect(css).not.toContain(`:root, [data-accent='rose']`); 60 | } 61 | ); 62 | }); 63 | 64 | it('correctly generates root color on the top of base styles.', () => { 65 | return generatePluginCss({ colors: ['sky', 'rose'], root: 'rose' }).then( 66 | (css) => { 67 | expect(css.indexOf(`:root, [data-accent='rose']`) === 0).toBeTruthy(); 68 | } 69 | ); 70 | }); 71 | }); 72 | 73 | describe('with tailwindcss v2', () => { 74 | it('returns nothing if colors option is not specified.', () => { 75 | return generatePluginCssV2().then((css) => { 76 | expect(css).toEqual(''); 77 | }); 78 | }); 79 | 80 | it('returns nothing if colors option is empty string.', () => { 81 | return generatePluginCssV2({ colors: '' }).then((css) => { 82 | expect(css).toEqual(''); 83 | }); 84 | }); 85 | 86 | it('returns nothing if colors option is empty array.', () => { 87 | return generatePluginCssV2({ colors: [] }).then((css) => { 88 | expect(css).toEqual(''); 89 | }); 90 | }); 91 | 92 | it('returns nothing if colors option is an array with not available colors.', () => { 93 | return generatePluginCssV2({ colors: ['maroon'] }).then((css) => { 94 | expect(css).toEqual(''); 95 | }); 96 | }); 97 | 98 | it('returns nothing if root option is specified and colors option is not specified.', () => { 99 | return generatePluginCssV2({ root: 'sky' }).then((css) => { 100 | expect(css).toEqual(''); 101 | }); 102 | }); 103 | 104 | it('correctly generates CSS custom properties.', () => { 105 | return generatePluginCssV2({ colors: ['rose'] }).then((css) => { 106 | expect(css).toContain(`--tw-ta-accent-500`); 107 | }); 108 | }); 109 | 110 | it('correctly generates customized CSS custom properties.', () => { 111 | return generatePluginCssV2({ 112 | colors: ['rose'], 113 | cssVarsPrefix: 'color', 114 | }).then((css) => { 115 | expect(css).toContain(`--color-accent-500`); 116 | }); 117 | }); 118 | 119 | it('correctly generates selected base style selectors.', () => { 120 | return generatePluginCssV2({ colors: ['rose'] }).then((css) => { 121 | expect(css).toContain(`[data-accent='rose']`); 122 | expect(css).not.toContain(`[data-accent='sky']`); 123 | }); 124 | }); 125 | 126 | it('correctly generates selected base style selectors with root.', () => { 127 | return generatePluginCssV2({ colors: ['sky', 'rose'], root: 'sky' }).then( 128 | (css) => { 129 | expect(css).toContain(`:root, [data-accent='sky']`); 130 | expect(css).not.toContain(`:root, [data-accent='rose']`); 131 | } 132 | ); 133 | }); 134 | 135 | it('correctly generates root color on the top of base styles.', () => { 136 | return generatePluginCssV2({ colors: ['sky', 'rose'], root: 'rose' }).then( 137 | (css) => { 138 | expect(css.indexOf(`:root, [data-accent='rose']`) === 0).toBeTruthy(); 139 | } 140 | ); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | const { hexToRgb, withOpacityValue } = require('../src/utils'); 2 | 3 | describe('hexToRgb()', () => { 4 | it('transform six digit hex.', () => { 5 | expect(hexToRgb('#ffffff')).toEqual('255 255 255'); 6 | }); 7 | 8 | it('transform three digit hex.', () => { 9 | expect(hexToRgb('#fff')).toEqual('255 255 255'); 10 | }); 11 | 12 | it('transform six digit hex without #.', () => { 13 | expect(hexToRgb('ffffff')).toEqual('255 255 255'); 14 | }); 15 | 16 | it('transform three digit hex without #.', () => { 17 | expect(hexToRgb('fff')).toEqual('255 255 255'); 18 | }); 19 | }); 20 | 21 | describe('withOpacityValue()', () => { 22 | it('transform to modern rgb color format with opacity.', () => { 23 | expect( 24 | withOpacityValue('--color-accent-200')({ 25 | opacityValue: 0.8, 26 | }) 27 | ).toEqual('rgb(var(--color-accent-200) / 0.8)'); 28 | }); 29 | 30 | it('transform to modern rgb color format without opacity.', () => { 31 | expect( 32 | withOpacityValue('--color-accent-200')({ 33 | opacityValue: undefined, 34 | }) 35 | ).toEqual('rgb(var(--color-accent-200))'); 36 | }); 37 | }); 38 | --------------------------------------------------------------------------------