├── .circleci └── config.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .tool-versions ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── examples ├── rect │ ├── README.md │ └── index.js └── resp-headings │ ├── README.md │ └── index.js ├── index.js ├── package.json ├── plugins ├── animate │ ├── README.md │ ├── index.js │ └── test.js ├── parent-expanded │ ├── README.md │ ├── index.js │ └── test.js ├── parent-open │ ├── README.md │ ├── index.js │ └── test.js ├── rect │ ├── README.md │ ├── index.js │ └── test.js ├── sr │ ├── README.md │ ├── index.js │ └── test.js └── text-underline-position │ ├── README.md │ ├── index.js │ └── test.js ├── testing ├── config.js └── run.js └── utilities ├── alpha ├── README.md └── index.js └── fns ├── README.md └── index.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/node:20.10.0 6 | steps: 7 | - checkout 8 | - run: 9 | name: Install JS 10 | command: | 11 | yarn install --frozen-lockfile 12 | - run: 13 | name: Test JS 14 | command: | 15 | yarn test 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.10.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.10.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - n/a 10 | 11 | ### Removed 12 | - n/a 13 | 14 | ### Changed 15 | - n/a 16 | 17 | ### Deprecated 18 | - Deprecate rect plugin 19 | - Deprecate sr plugin 20 | 21 | ## [3.0.0] - 06-13-2022 22 | ### Added 23 | - Added the text-underline-position plugin 24 | 25 | ### Removed 26 | - Removed animation plugin 27 | - Removed background plugin 28 | - Removed blend plugin 29 | - Removed filter plugin 30 | - Removed flex-basis plugin 31 | - Removed gradient plugin 32 | 33 | ### Changed 34 | - Updated Tailwind to v3 35 | - Updated tests for Tailwind v3 36 | - Updated dependencies 37 | 38 | ### Deprecated 39 | - n/a 40 | 41 | ## [0.0.11] - 09-15-2020 42 | ### Fixed 43 | - Fixed missing export, missing README update 44 | 45 | ## [0.0.10] - 09-15-2020 46 | ### Added 47 | - Added the animate plugin 48 | 49 | ## [0.0.9] - 08-25-2020 50 | ### Added 51 | - Added .sr-undo-absolute utility 52 | - Added testing via CircleCI 53 | 54 | ## [0.0.8] - 05-06-2020 55 | ### Fixed 56 | - Fixed gradients plugin 57 | 58 | ## [0.0.7] - 05-05-2020 59 | ### Added 60 | - Added utility functions 61 | 62 | ### Fixed 63 | - Fixed missing top-level exports 64 | 65 | ## [0.0.6] - 05-04-2020 66 | ### Added 67 | - Added gradients plugin 68 | 69 | ## [0.0.5] - 04-28-2020 70 | ### Fixed 71 | - Fixed filters plugin 72 | 73 | ## [0.0.4] - 04-27-2020 74 | ### Added 75 | - Added prettier and formatted project 76 | - Added animation, background and filter plugins 77 | - Added alpha.js as requireable utility 78 | 79 | ## [0.0.3] - 04-14-2020 80 | ### Fixed 81 | - Fixed flex basis plugin 82 | 83 | ## [0.0.2] - 04-14-2020 84 | ### Changed 85 | - Modified index.js exports to allow importing from top level 86 | 87 | ## [0.0.1] - 04-14-2020 88 | Initial release 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please review this document before submitting a pull request. 4 | 5 | ## Setup 6 | 7 | To get started with local development, 8 | 9 | 1. Install the correct version of Node. 10 | 11 | ```bash 12 | asdf install 13 | ``` 14 | 15 | 1. Install `package.json` dependencies. Versions are locked in `yarn.lock`, so installing them with Yarn is recommended. 16 | 17 | ```bash 18 | yarn 19 | ```` 20 | 21 | See our [How to use local Node packages as project dependencies](https://www.viget.com/articles/how-to-use-local-unpublished-node-packages-as-project-dependencies/) article to use your development version in a project. 22 | 23 | ## Coding standards 24 | 25 | Our code formatting rules are defined in .prettierrc. You can format your code against these standards using [Prettier](https://prettier.io/) via the `format` script in `package.json`. 26 | 27 | ```bash 28 | yarn format 29 | ``` 30 | 31 | ## Running tests 32 | 33 | To test all plugins, run the `test` script in `package.json`. 34 | 35 | ```bash 36 | yarn test 37 | ``` 38 | 39 | To test a specific plugin, run the `test` script in `package.json` passing it the test file’s path. 40 | 41 | ```bash 42 | yarn test plugins/my-plugin/test 43 | ``` 44 | 45 | ## Writing tests 46 | 47 | Each plugin is tested using [Jest](https://jestjs.io/). The easiest way to get started is by looking at existing tests. 48 | 49 | ### Structure 50 | 51 | - **Output**: Define what the generated CSS should look like. 52 | - **Config**: Define the content string to use for generating the CSS. Most plugins require a theme passed as well. 53 | 54 | ## Documenting changes and releases 55 | 56 | - Add notes to the unreleased section in the CHANGELOG, with summaries of any additions, fixes, updates, or deprecations. 57 | - When a new version is released, a maintainer will move those notes to the appropriate version number in the CHANGELOG. 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS Plugins 2 | 3 | This plugin wraps up a collection of other plugins we've written for [Tailwind CSS](https://tailwindcss.com/). 4 | 5 | Plugins include: 6 | * [animate](/plugins/animate/) 7 | * [parent-expanded](/plugins/parent-expanded/) 8 | * [parent-open](/plugins/parent-open/) 9 | * [rect](/plugins/rect/) 10 | * [sr](/plugins/sr/) 11 | * [text-underline-position](/plugins/text-underline-position/) 12 | 13 | Utilities include: 14 | * [hex alpha reference](/utilities/alpha/) 15 | * [helper functions](/utilities/fns/) 16 | 17 | ## Installation 18 | 19 | Add `@viget/tailwindcss-plugins` to `devDependencies` in `package.json`. 20 | 21 | ```bash 22 | yarn add -D @viget/tailwindcss-plugins 23 | ``` 24 | 25 | ## Usage 26 | 27 | Simply require the plugins or utilities in your `tailwindcss.config.js` file, and follow the usage instructions in each plugin’s README. 28 | 29 | ```js 30 | // utilities 31 | const a = require('@viget/tailwindcss-plugins/utilities/alpha') 32 | const { em, rem, remPair, pxPair } = require('@viget/tailwindcss-plugins/utilities/fns') 33 | const plugins = require('@viget/tailwindcss-plugins') 34 | 35 | // plugins 36 | plugins: [ 37 | plugins.rect, 38 | plugins.sr, 39 | // ... 40 | ], 41 | ``` 42 | 43 | ## Notes 44 | 45 | There are some additional plugin examples included in this repository’s **examples** directory. These are not registerable in your Tailwind config. The reasons for this are that these represent plugins which: 46 | 47 | * could have a number of different outputs that require varying approaches 48 | * have a highly specific project use case 49 | * are an alternate approach to one of the registerable plugins 50 | 51 | For example, heading components could be done any number of ways. They could be simple or complex, fixed or responsive, wrapped for rich text or classnames only. For this reason, we've included several examples of these. If you need one of these plugins, it is recommended to copy and paste the plugin code into a custom plugin for your project. 52 | 53 | --- 54 | 55 | 56 | Code At Viget 57 | 58 | 59 | Visit [code.viget.com](http://code.viget.com) to see more projects from [Viget.](https://viget.com) 60 | -------------------------------------------------------------------------------- /examples/rect/README.md: -------------------------------------------------------------------------------- 1 | # rect 2 | 3 | _This example has been **deprecated**. Consider using new [size utilities](https://tailwindcss.com/blog/tailwindcss-v3-4#new-size-utilities) in Tailwind 3.4 instead._ 4 | 5 | This plugin adds rect (width + height) sizing utilities to Tailwind. It is different from the [installable version](plugins/rect) in that it removes the dependancy on the rem function and allows for edge cases requiring other units like px for sizing. 6 | 7 | ## Usage 8 | 9 | ```js 10 | theme: { 11 | rect: { 12 | target: ['44px', '44px'], 13 | '24': ['24px', '24px'], 14 | }, 15 | } 16 | plugins: [ 17 | require('./path/to/your/plugin'), 18 | // ... 19 | ], 20 | ``` 21 | 22 | The above configuration would create the following css, as well as their responsive variants: 23 | 24 | ```css 25 | .rect-target { 26 | width: 44px; 27 | height: 44px; 28 | } 29 | .rect-24 { 30 | width: 24px; 31 | height: 24px; 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /examples/rect/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = plugin(({ matchUtilities, theme }) => { 4 | matchUtilities( 5 | { 6 | rect: (value) => ({ 7 | width: value[0], 8 | height: value[1], 9 | }), 10 | }, 11 | { values: theme('rect') } 12 | ) 13 | }) 14 | -------------------------------------------------------------------------------- /examples/resp-headings/README.md: -------------------------------------------------------------------------------- 1 | # resp-headings 2 | 3 | This plugin adds responsive heading components to Tailwind. 4 | 5 | The limitation to this plugin is that you cannot use `@apply .heading-key`. Tailwind explicitly prevents the use of `@apply` with "complex" components with media queries. 6 | 7 | ## Usage 8 | 9 | ```js 10 | theme: { 11 | headings: theme => ({ 12 | '24': { 13 | sizes: { 14 | default: theme('fontSize.20'), 15 | md: theme('fontSize.24') 16 | }, 17 | lineHeight: theme('lineHeight.snug'), 18 | weight: theme('fontWeight.bold') 19 | }, 20 | }) 21 | }, 22 | plugins: [ 23 | require('./config/tailwind/plugins/resp-headings'), 24 | ], 25 | ``` 26 | 27 | Note: With the exception of `default`, the keys on the `sizes` object should match the keys in your `screens` Tailwind configuration. 28 | 29 | The above configuration would create the following css: 30 | 31 | ```css 32 | .heading-24 { 33 | font-size: 20px; 34 | font-weight: bold; 35 | letter-spacing: 2px; 36 | line-height: 1.4; 37 | } 38 | 39 | @media (min-width: 768px) { 40 | .heading-24 { 41 | font-size: 24px; 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /examples/resp-headings/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = plugin(({ addComponents, e, theme }) => { 4 | const screens = theme('screens') 5 | const pluginConfig = theme('headings', {}) 6 | 7 | const component = Object.entries(pluginConfig).map(([name, props]) => { 8 | const { sizes, letterSpacing = '0', lineHeight, weight } = props 9 | const mediaQueries = [] 10 | 11 | for (const key in screens) { 12 | if (key in sizes) { 13 | mediaQueries.push({ 14 | [`@media (min-width: ${screens[key]})`]: { 15 | [`.${e(`heading-${name}`)}`]: { 16 | 'font-size': sizes[key] 17 | } 18 | } 19 | }) 20 | } 21 | } 22 | 23 | return [ 24 | { 25 | [`.${e(`heading-${name}`)}`]: { 26 | 'font-size': sizes['default'], 27 | 'font-weight': weight, 28 | 'letter-spacing': letterSpacing, 29 | 'line-height': lineHeight 30 | } 31 | }, 32 | ...mediaQueries 33 | ] 34 | }) 35 | 36 | addComponents(component) 37 | }) 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | animate: require('./plugins/animate'), 3 | parentExpanded: require('./plugins/parent-expanded'), 4 | parentOpen: require('./plugins/parent-open'), 5 | rect: require('./plugins/rect'), 6 | sr: require('./plugins/sr'), 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@viget/tailwindcss-plugins", 3 | "version": "3.0.0", 4 | "scripts": { 5 | "format": "prettier --write './**/*.js' --ignore-path .gitignore", 6 | "test": "jest" 7 | }, 8 | "description": "A collection of custom plugins for Tailwind CSS.", 9 | "main": "index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/vigetlabs/tailwindcss-plugins.git" 13 | }, 14 | "keywords": [ 15 | "tailwind", 16 | "tailwindcss", 17 | "tailwindcss-plugin" 18 | ], 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/vigetlabs/tailwindcss-plugins/issues" 22 | }, 23 | "homepage": "https://github.com/vigetlabs/tailwindcss-plugins#readme", 24 | "devDependencies": { 25 | "jest": "^27.5.1", 26 | "jest-matcher-css": "^1.1.0", 27 | "lodash": "^4.17.21", 28 | "postcss": "^8.4.12", 29 | "prettier": "^2.6.2", 30 | "tailwindcss": "^3.0.24" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugins/animate/README.md: -------------------------------------------------------------------------------- 1 | # animate 2 | 3 | This plugin adds utilities for animating elements based on a dynamically added class. The main use case this addresses is animating elements in during scrolling. By adding a class via [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) or some other scroll listener, these utilities allow you to easily define the animations and apply staggered transitions. This has also been designed to work with the existing Tailwind utilities that control transition properties. 4 | 5 | ## Usage 6 | 7 | ### Config Example 8 | 9 | ```js 10 | theme: { 11 | animate: (theme) => ({ 12 | triggerClass: '-observed', 13 | staggerDelay: { 14 | '100': '100ms', 15 | '200': '200ms', 16 | ...theme('transitionDelay'), 17 | }, 18 | staggerInterval: { 19 | default: '100ms', 20 | '200': '200ms', 21 | ...theme('transitionDelay'), 22 | }, 23 | maxItemIntervalSupport: 9, 24 | animations: { 25 | 'fade-up': { 26 | from: { 27 | transform: 'translateY(20px)', 28 | opacity: 0, 29 | }, 30 | to: { 31 | transform: 'translateY(0)', 32 | opacity: 1, 33 | }, 34 | }, 35 | 'zoom-in': { 36 | from: { 37 | transform: 'scale(0.8)', 38 | opacity: 0, 39 | }, 40 | // "to" is optional 41 | }, 42 | }, 43 | }), 44 | }, 45 | plugins: [ 46 | require('@viget/tailwindcss-plugins/animate'), 47 | ], 48 | ``` 49 | 50 | ### Markup Examples 51 | 52 | #### Animate a single element (pending addition of `triggerClass`) 53 | 54 | ```html 55 |
Hello!
56 | ``` 57 | 58 | --- 59 | 60 | #### Animate a single element after a delay 61 | ```html 62 |
Hello
63 | ``` 64 | > N.b. `delay-` is a first-party Tailwind utility 65 | 66 | --- 67 | 68 | #### Stagger the animation of multiple elements, using the specified default interval 69 | ```html 70 | 75 | ``` 76 | > N.b. `duration-` is a first-party Tailwind utility and **is required** on the child element unless a `transition-duration` is otherwise specified. This plugin doesn not apply a default duration in order to preserve customizability using the `duration-` utilities. 77 | 78 | --- 79 | 80 | #### Stagger the animation of multiple elements, overriding default interval 81 | ```html 82 | 87 | ``` 88 | 89 | --- 90 | 91 | #### Stagger the animation of multiple elements, using specified interval, but delay the start 92 | ```html 93 | 98 | ``` 99 | 100 | ## Configuration 101 | 102 | ### `triggerClass` 103 | 104 | Specify the class name that will be dynamically added to the element. Typically this class is added to indicate that the element has entered the viewport. 105 | 106 | ### `staggerDelay` 107 | 108 | Specify the delays for starting the staggered animations. It probably makes the most sense to simply set this to `theme('transitionDelay')`. 109 | 110 | ### `staggerInterval` 111 | 112 | Specify the amount of time in between the staggered animations. While optional, it is recommended to add a `default` entry to this object. Doing so allows you to use the `stagger-[animation]` class without specifying any `stagger-interval-[time]`. 113 | 114 | ### `maxItemIntervalSupport` 115 | 116 | The `transition-delay` used to power the staggered animation are calculated via a custom property on the child elements called `--animate-index`. This setting allows you to decide how many `nth-child` selectors should be automatically output with this custom property so you do not need to add it by hand. 117 | 118 | ### `animations` 119 | 120 | Specify the animation styles. The first-level object names the animation, which in turn **must** specify a `from` object and optionally a `to` object. These objects accept the same [CSS-in-JS syntax as Tailwind](https://tailwindcss.com/docs/plugins#css-in-js-syntax). -------------------------------------------------------------------------------- /plugins/animate/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = plugin(({ addUtilities, e, theme }) => { 4 | const pluginConfig = theme('animate', {}) 5 | const { 6 | triggerClass, 7 | staggerDelay, 8 | staggerInterval, 9 | maxItemIntervalSupport, 10 | animations, 11 | } = pluginConfig 12 | 13 | const animationUtilities = Object.entries(animations).map( 14 | ([name, config]) => { 15 | const from = { 16 | [`.${e(`animate-${name}`)}:not(.${e(triggerClass)}), .${e( 17 | `stagger-${name}`, 18 | )}:not(.${e(triggerClass)}) > *`]: config.from, 19 | } 20 | 21 | const to = config.to 22 | ? { 23 | [`.${e(`animate-${name}`)}.${e(triggerClass)},.${e( 24 | `stagger-${name}`, 25 | )}.${triggerClass} > *`]: config.to, 26 | } 27 | : {} 28 | 29 | return { 30 | ...from, 31 | ...to, 32 | } 33 | }, 34 | ) 35 | 36 | const staggerDefaultUtility = staggerInterval.default 37 | ? [ 38 | { 39 | '[class*="stagger-"] > *': { 40 | '--stagger-delay': '0s', 41 | 'transition-delay': `calc(var(--animate-index) * ${staggerInterval.default} + var(--stagger-delay))`, 42 | }, 43 | }, 44 | ] 45 | : [] 46 | 47 | const staggerIntervalUtilities = Object.entries(staggerInterval) 48 | .filter(([name]) => name !== 'default') 49 | .map(([name, value]) => ({ 50 | [`.${e(`stagger-interval-${name}`)} > *`]: { 51 | '--stagger-delay': '0s', 52 | 'transition-delay': `calc(var(--animate-index) * ${value} + var(--stagger-delay))`, 53 | }, 54 | })) 55 | 56 | const staggerDelayUtilities = Object.entries(staggerDelay).map( 57 | ([name, value]) => ({ 58 | [`.${e(`stagger-delay-${name}`)} > *`]: { 59 | '--stagger-delay': value, 60 | }, 61 | }), 62 | ) 63 | 64 | const nthChildUtilities = [...Array(maxItemIntervalSupport)].map((_, i) => ({ 65 | [`[class*="stagger"] > *:nth-child(${i + 1})`]: { 66 | '--animate-index': `${i + 1}`, 67 | }, 68 | })) 69 | 70 | addUtilities([ 71 | ...animationUtilities, 72 | ...staggerDefaultUtility, 73 | ...staggerIntervalUtilities, 74 | ...staggerDelayUtilities, 75 | ...nthChildUtilities, 76 | ]) 77 | }) 78 | -------------------------------------------------------------------------------- /plugins/animate/test.js: -------------------------------------------------------------------------------- 1 | const cssMatcher = require('jest-matcher-css') 2 | const plugin = require('./index') 3 | const { run } = require('../../testing/run') 4 | 5 | expect.extend({ 6 | toMatchCss: cssMatcher, 7 | }) 8 | 9 | it('should generate the animate classes', () => { 10 | const config = { 11 | content: [ 12 | { 13 | raw: String.raw` 14 | 19 | `, 20 | }, 21 | ], 22 | theme: { 23 | animate: { 24 | triggerClass: '-observed', 25 | staggerDelay: { 26 | '200': '200ms', 27 | }, 28 | staggerInterval: { 29 | '200': '200ms', 30 | }, 31 | maxItemIntervalSupport: 5, 32 | animations: { 33 | 'fade-left': { 34 | from: { 35 | transform: 'translateX(-20px)', 36 | opacity: 0, 37 | }, 38 | to: { 39 | transform: 'translateX(0)', 40 | opacity: 1, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | } 47 | 48 | const output = String.raw` 49 | .duration-500 { 50 | transition-duration: 500ms; 51 | } 52 | 53 | .animate-fade-left:not(.-observed), .stagger-fade-left:not(.-observed) > * { 54 | transform: translateX(-20px); 55 | opacity: 0; 56 | } 57 | 58 | .animate-fade-left.-observed,.stagger-fade-left.-observed > * { 59 | transform: translateX(0); 60 | opacity: 1; 61 | } 62 | 63 | .stagger-interval-200 > * { 64 | --stagger-delay: 0s; 65 | transition-delay: calc(var(--animate-index) * 200ms + var(--stagger-delay)); 66 | } 67 | 68 | .stagger-delay-200 > * { 69 | --stagger-delay: 200ms; 70 | } 71 | 72 | [class*="stagger"] > *:nth-child(1) { 73 | --animate-index: 1; 74 | } 75 | 76 | [class*="stagger"] > *:nth-child(2) { 77 | --animate-index: 2; 78 | } 79 | 80 | [class*="stagger"] > *:nth-child(3) { 81 | --animate-index: 3; 82 | } 83 | 84 | [class*="stagger"] > *:nth-child(4) { 85 | --animate-index: 4; 86 | } 87 | 88 | [class*="stagger"] > *:nth-child(5) { 89 | --animate-index: 5; 90 | } 91 | 92 | ` 93 | 94 | expect.assertions(2) 95 | return run(plugin, config).then((result) => { 96 | expect(result.warnings().length).toBe(0) 97 | expect(result.css).toMatchCss(output) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /plugins/parent-expanded/README.md: -------------------------------------------------------------------------------- 1 | # parent-expanded 2 | 3 | This plugin adds parent-expanded variants to Tailwind. This is useful for accessibility approaches using `aria-expanded="true"`. 4 | 5 | ## Usage 6 | 7 | ```js 8 | const plugins = require('@viget/tailwindcss-plugins') 9 | 10 | module.exports = { 11 | theme: { 12 | rotate: { 13 | '180': '180deg', 14 | }, 15 | }, 16 | plugins: [ 17 | plugins.parentExpanded, 18 | // ... 19 | ], 20 | } 21 | ``` 22 | 23 | The above configuration would create the following css: 24 | 25 | ```css 26 | .rotate-180 { 27 | --transform-rotate: 180deg; 28 | } 29 | 30 | [aria-expanded="true"] .parent-expanded\:rotate-180 { 31 | --transform-rotate: 180deg; 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /plugins/parent-expanded/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = plugin(({ addVariant }) => { 4 | addVariant('parent-expanded', '[aria-expanded="true"] &') 5 | }) 6 | -------------------------------------------------------------------------------- /plugins/parent-expanded/test.js: -------------------------------------------------------------------------------- 1 | const cssMatcher = require('jest-matcher-css') 2 | const plugin = require('./index') 3 | const { run } = require('../../testing/run') 4 | 5 | expect.extend({ 6 | toMatchCss: cssMatcher, 7 | }) 8 | 9 | it('should generate the parent-expanded classes', () => { 10 | const config = { 11 | content: [ 12 | { 13 | raw: String.raw` 14 | 17 | `, 18 | }, 19 | ], 20 | } 21 | 22 | const output = String.raw` 23 | [aria-expanded="true"] .parent-expanded\:rotate-180 { 24 | --tw-rotate: 180deg; 25 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) 26 | } 27 | ` 28 | 29 | expect.assertions(2) 30 | return run(plugin, config).then((result) => { 31 | expect(result.warnings().length).toBe(0) 32 | expect(result.css).toMatchCss(output) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /plugins/parent-open/README.md: -------------------------------------------------------------------------------- 1 | # parent-open 2 | 3 | This plugin adds parent-open variants to Tailwind. This is intended for use with the [HTML Details Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details). 4 | 5 | ## Usage 6 | 7 | ```js 8 | const plugins = require('@viget/tailwindcss-plugins') 9 | 10 | module.exports = { 11 | theme: { 12 | rotate: { 13 | '180': '180deg', 14 | }, 15 | }, 16 | plugins: [ 17 | plugins.parentOpen, 18 | // ... 19 | ], 20 | } 21 | ``` 22 | 23 | The above configuration would create the following css: 24 | 25 | ```css 26 | .rotate-180 { 27 | --transform-rotate: 180deg; 28 | } 29 | 30 | [open] .parent-open\:rotate-180 { 31 | --transform-rotate: 180deg; 32 | } 33 | ``` 34 | 35 | Example: 36 | 37 | ```html 38 |
39 | 40 | 41 | 42 |

...

43 |
44 | ``` 45 | 46 | ## Alternate Approaches 47 | 48 | This can also be acheived with some more verbose approaches with Tailwind. The `open` [modifier](https://tailwindcss.com/docs/hover-focus-and-other-states#open-closed-state) is intended for use with `
` and `` elements, but modifying child elements is not as straightforward. 49 | 50 | One option is to use the group modifier for [styling the element based on the parent state](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state). 51 | 52 | ```html 53 |
54 | 55 | 56 | 57 |

...

58 |
59 | ``` 60 | 61 | Another option is to use an [arbitrary variant](https://tailwindcss.com/docs/hover-focus-and-other-states#using-arbitrary-variants) with the open modifier to target the element from the parent. 62 | 63 | ```html 64 |
65 | 66 | 67 | 68 |

...

69 |
70 | ``` 71 | -------------------------------------------------------------------------------- /plugins/parent-open/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = plugin(({ addVariant }) => { 4 | addVariant('parent-open', '[open] &') 5 | }) 6 | -------------------------------------------------------------------------------- /plugins/parent-open/test.js: -------------------------------------------------------------------------------- 1 | const cssMatcher = require('jest-matcher-css') 2 | const plugin = require('./index') 3 | const { run } = require('../../testing/run') 4 | 5 | expect.extend({ 6 | toMatchCss: cssMatcher, 7 | }) 8 | 9 | it('should generate the parent-open classes', () => { 10 | const config = { 11 | content: [ 12 | { 13 | raw: String.raw` 14 |
15 | 16 | > 17 | 18 |
19 | `, 20 | }, 21 | ], 22 | } 23 | 24 | const output = String.raw` 25 | [open] .parent-open\:rotate-180 { 26 | --tw-rotate: 180deg; 27 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) 28 | } 29 | ` 30 | 31 | expect.assertions(2) 32 | return run(plugin, config).then((result) => { 33 | expect(result.warnings().length).toBe(0) 34 | expect(result.css).toMatchCss(output) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /plugins/rect/README.md: -------------------------------------------------------------------------------- 1 | # rect 2 | 3 | _This plugin has been **deprecated**. Consider using new [size utilities](https://tailwindcss.com/blog/tailwindcss-v3-4#new-size-utilities) in Tailwind 3.4 instead._ 4 | 5 | This plugin adds rect (width + height) sizing utilities to Tailwind. 6 | 7 | ## Usage 8 | 9 | ```js 10 | const plugins = require('@viget/tailwindcss-plugins') 11 | 12 | module.exports = { 13 | theme: { 14 | rect: { 15 | target: [44, 44], 16 | '24': [24, 24], 17 | }, 18 | }, 19 | plugins: [ 20 | plugins.rect, 21 | // ... 22 | ], 23 | } 24 | ``` 25 | 26 | The above configuration would create the following css, as well as their responsive variants: 27 | 28 | ```css 29 | .rect-target { 30 | width: 2.75rem; 31 | height: 2.75rem; 32 | } 33 | .rect-24 { 34 | width: 1.5rem; 35 | height: 1.5rem; 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /plugins/rect/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | const { rem } = require('../../utilities/fns') 3 | 4 | module.exports = plugin(({ matchUtilities, theme }) => { 5 | matchUtilities( 6 | { 7 | rect: (value) => ({ 8 | width: rem(value[0]), 9 | height: rem(value[1]), 10 | }), 11 | }, 12 | { values: theme('rect') } 13 | ) 14 | }) 15 | -------------------------------------------------------------------------------- /plugins/rect/test.js: -------------------------------------------------------------------------------- 1 | const cssMatcher = require('jest-matcher-css') 2 | const plugin = require('./index') 3 | const { run } = require('../../testing/run') 4 | 5 | expect.extend({ 6 | toMatchCss: cssMatcher, 7 | }) 8 | 9 | it('should generate the rect classes', () => { 10 | const config = { 11 | content: [ 12 | { 13 | raw: String.raw` 14 | 15 | `, 16 | }, 17 | ], 18 | theme: { 19 | rect: { 20 | 44: [44, 44], 21 | }, 22 | }, 23 | } 24 | 25 | const output = String.raw` 26 | .rect-44 { 27 | width: 2.75rem; 28 | height: 2.75rem; 29 | } 30 | ` 31 | 32 | expect.assertions(2) 33 | return run(plugin, config).then((result) => { 34 | expect(result.warnings().length).toBe(0) 35 | expect(result.css).toMatchCss(output) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /plugins/sr/README.md: -------------------------------------------------------------------------------- 1 | # sr 2 | 3 | _This plugin has been **deprecated**. According to the latest comments in the [original h5bp issue thread](https://github.com/h5bp/main.css/issues/12) (linked from the blog post in the below issue description), the out-of-order reading is no longer occuring in macOS VoiceOver. It is now safe to use the original sr utilities in Tailwind!_ 4 | 5 | This plugin adds screenreader utilities to Tailwind. Tailwind ships with its own variation, so you need to **turn off `accessibility` in the `corePlugins`.** 6 | 7 | [Read more discussion here](https://github.com/tailwindcss/tailwindcss/pull/964) on the default implementation compared to this plugin. 8 | 9 | ## Usage 10 | 11 | ```js 12 | const plugins = require('@viget/tailwindcss-plugins') 13 | 14 | module.exports = { 15 | corePlugins: [ 16 | accessibility: false, 17 | ], 18 | plugins: [ 19 | plugins.sr, 20 | // ... 21 | ], 22 | } 23 | ``` 24 | 25 | The above configuration would create the following css, as well as their responsive variants: 26 | 27 | ```css 28 | .sr-only { 29 | border: 0; 30 | clip: rect(0 0 0 0); 31 | height: 1px; 32 | margin: -1px; 33 | overflow: hidden; 34 | padding: 0px; 35 | position: absolute; 36 | width: 1px; 37 | } 38 | 39 | .sr-undo { 40 | clip: auto; 41 | height: auto; 42 | margin: 0; 43 | overflow: visible; 44 | position: static; 45 | width: auto; 46 | } 47 | 48 | .sr-undo-absolute { 49 | clip: auto; 50 | height: auto; 51 | margin: 0; 52 | overflow: visible; 53 | position: absolute; 54 | width: auto; 55 | } 56 | ``` 57 | 58 | ## Skip Links 59 | 60 | The `.sr-undo-absolute` utility is especially helpful for skip links, when you want to preserve `position: absolute`. 61 | 62 | For example: 63 | 64 | ```html 65 | Skip to Main Content 66 | ``` -------------------------------------------------------------------------------- /plugins/sr/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = plugin(({ addUtilities }) => { 4 | const sr = { 5 | '.sr-only': { 6 | border: '0', 7 | clip: 'rect(0 0 0 0)', 8 | height: '1px', 9 | margin: '-1px', 10 | overflow: 'hidden', 11 | padding: '0', 12 | position: 'absolute', 13 | width: '1px', 14 | }, 15 | '.sr-undo': { 16 | clip: 'auto', 17 | height: 'auto', 18 | margin: '0', 19 | overflow: 'visible', 20 | position: 'static', 21 | width: 'auto', 22 | }, 23 | '.sr-undo-absolute': { 24 | clip: 'auto', 25 | height: 'auto', 26 | margin: '0', 27 | overflow: 'visible', 28 | position: 'absolute', 29 | width: 'auto', 30 | }, 31 | } 32 | 33 | addUtilities(sr) 34 | }) 35 | -------------------------------------------------------------------------------- /plugins/sr/test.js: -------------------------------------------------------------------------------- 1 | const cssMatcher = require('jest-matcher-css') 2 | const plugin = require('./index') 3 | const { run } = require('../../testing/run') 4 | 5 | expect.extend({ 6 | toMatchCss: cssMatcher, 7 | }) 8 | 9 | it('should generate the sr classes', () => { 10 | const config = { 11 | content: [ 12 | { 13 | raw: String.raw` 14 | text 15 | text 16 | text 17 | `, 18 | }, 19 | ], 20 | corePlugins: { 21 | accessibility: false, 22 | }, 23 | } 24 | 25 | const output = String.raw` 26 | .sr-only { 27 | border: 0; 28 | clip: rect(0 0 0 0); 29 | height: 1px; 30 | margin: -1px; 31 | overflow: hidden; 32 | padding: 0; 33 | position: absolute; 34 | width: 1px; 35 | } 36 | .sr-undo { 37 | clip: auto; 38 | height: auto; 39 | margin: 0; 40 | overflow: visible; 41 | position: static; 42 | width: auto; 43 | } 44 | .sr-undo-absolute { 45 | clip: auto; 46 | height: auto; 47 | margin: 0; 48 | overflow: visible; 49 | position: absolute; 50 | width: auto; 51 | } 52 | ` 53 | 54 | expect.assertions(2) 55 | return run(plugin, config).then((result) => { 56 | expect(result.warnings().length).toBe(0) 57 | expect(result.css).toMatchCss(output) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /plugins/text-underline-position/README.md: -------------------------------------------------------------------------------- 1 | # text-underline-position 2 | 3 | This plugin adds `text-underline-position` utilities to Tailwind. 4 | 5 | ## Usage 6 | 7 | ```js 8 | plugins: [ 9 | require('@viget/tailwindcss-plugins/text-underline-position'), 10 | // ... 11 | ], 12 | ``` 13 | 14 | The plugin will enable the following css, as well as their responsive variants: 15 | 16 | ```css 17 | .underline-auto { 18 | text-underline-position: auto; 19 | } 20 | .underline-auto-left { 21 | text-underline-position: auto left; 22 | } 23 | .underline-auto-right { 24 | text-underline-position: auto right; 25 | } 26 | .underline-auto-under { 27 | text-underline-position: auto under; 28 | } 29 | .underline-from-font { 30 | text-underline-position: from-font; 31 | } 32 | .underline-from-font-left { 33 | text-underline-position: from-font left; 34 | } 35 | .underline-from-font-right { 36 | text-underline-position: from-font right; 37 | } 38 | .underline-from-font-under { 39 | text-underline-position: from-font under; 40 | } 41 | .underline-left { 42 | text-underline-position: left; 43 | } 44 | .underline-right { 45 | text-underline-position: right; 46 | } 47 | .underline-under { 48 | text-underline-position: under; 49 | } 50 | .underline-inherit { 51 | text-underline-position: inherit; 52 | } 53 | .underline-initial { 54 | text-underline-position: initial; 55 | } 56 | .underline-revert { 57 | text-underline-position: revert; 58 | } 59 | .underline-unset { 60 | text-underline-position: unset; 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /plugins/text-underline-position/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tailwind CSS plugin to generate text-underline-position utilities 3 | * https://developer.mozilla.org/en-US/docs/Web/CSS/text-underline-position 4 | */ 5 | 6 | const plugin = require('tailwindcss/plugin') 7 | 8 | module.exports = plugin(({ addUtilities }) => { 9 | const utilities = {} 10 | 11 | const placements = ['auto', 'from-font'] 12 | 13 | const sides = ['left', 'right', 'under'] 14 | 15 | const sidesAndGlobals = [...sides, 'inherit', 'initial', 'revert', 'unset'] 16 | 17 | placements.forEach((p) => { 18 | utilities[`.underline-${p}`] = { 19 | textUnderlinePosition: p, 20 | } 21 | 22 | sides.forEach((s) => { 23 | utilities[`.underline-${p}-${s}`] = { 24 | textUnderlinePosition: `${p} ${s}`, 25 | } 26 | }) 27 | }) 28 | 29 | sidesAndGlobals.forEach((s) => { 30 | utilities[`.underline-${s}`] = { 31 | textUnderlinePosition: s, 32 | } 33 | }) 34 | 35 | addUtilities(utilities) 36 | }) 37 | -------------------------------------------------------------------------------- /plugins/text-underline-position/test.js: -------------------------------------------------------------------------------- 1 | const cssMatcher = require('jest-matcher-css') 2 | const plugin = require('./index') 3 | const { run } = require('../../testing/run') 4 | 5 | expect.extend({ 6 | toMatchCss: cssMatcher, 7 | }) 8 | 9 | it('should generate the text-underline-position classes', () => { 10 | const config = { 11 | content: [ 12 | { 13 | raw: String.raw` 14 | Text 15 | Text 16 | Text 17 | Text 18 | Text 19 | Text 20 | Text 21 | Text 22 | Text 23 | Text 24 | Text 25 | Text 26 | Text 27 | Text 28 | Text 29 | `, 30 | }, 31 | ], 32 | } 33 | 34 | const output = String.raw` 35 | .underline-auto { 36 | text-underline-position: auto; 37 | } 38 | .underline-auto-left { 39 | text-underline-position: auto left; 40 | } 41 | .underline-auto-right { 42 | text-underline-position: auto right; 43 | } 44 | .underline-auto-under { 45 | text-underline-position: auto under; 46 | } 47 | .underline-from-font { 48 | text-underline-position: from-font; 49 | } 50 | .underline-from-font-left { 51 | text-underline-position: from-font left; 52 | } 53 | .underline-from-font-right { 54 | text-underline-position: from-font right; 55 | } 56 | .underline-from-font-under { 57 | text-underline-position: from-font under; 58 | } 59 | .underline-left { 60 | text-underline-position: left; 61 | } 62 | .underline-right { 63 | text-underline-position: right; 64 | } 65 | .underline-under { 66 | text-underline-position: under; 67 | } 68 | .underline-inherit { 69 | text-underline-position: inherit; 70 | } 71 | .underline-initial { 72 | text-underline-position: initial; 73 | } 74 | .underline-revert { 75 | text-underline-position: revert; 76 | } 77 | .underline-unset { 78 | text-underline-position: unset; 79 | } 80 | ` 81 | 82 | expect.assertions(2) 83 | return run(plugin, config).then((result) => { 84 | expect(result.warnings().length).toBe(0) 85 | expect(result.css).toMatchCss(output) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /testing/config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('tailwindcss/defaultConfig') 2 | const merge = require('lodash/merge') 3 | 4 | module.exports = function (plugin, userConfig = {}, useDefaultConfig = false) { 5 | const testConfig = { 6 | corePlugins: false, 7 | plugins: [plugin], 8 | } 9 | 10 | return useDefaultConfig 11 | ? merge({}, defaultConfig, testConfig, userConfig) 12 | : merge({}, testConfig, userConfig) 13 | } 14 | -------------------------------------------------------------------------------- /testing/run.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const postcss = require('postcss') 3 | const tailwindcss = require('tailwindcss') 4 | 5 | module.exports = { 6 | run: function (plugin, config) { 7 | let { currentTestName } = expect.getState() 8 | 9 | let input = String.raw` 10 | @tailwind components; 11 | @tailwind utilities; 12 | ` 13 | config = { 14 | ...{ plugins: [plugin], corePlugins: { preflight: false } }, 15 | ...config, 16 | } 17 | 18 | return postcss(tailwindcss(config)).process(input, { 19 | from: `${path.resolve(__filename)}?test=${currentTestName}`, 20 | }) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /utilities/alpha/README.md: -------------------------------------------------------------------------------- 1 | # alpha 2 | 3 | There are many options for manipulating the transparency of colors in your Tailwind config, but one straightforward method is to use alpha hex colors. These are part of the [new CSS spec](https://www.w3.org/TR/css-color-4/#hex-notation) and work by adding an alpha channel to the end of a hex value. 4 | 5 | For three-digit hex values, like `#fff`, this is a single value: `#fffc`. For six-digits, two values are added: `#ffffffcc`. Unfortunately, these alpha values must be written in hexadecimal, so something like 75% would actually be written as `bf`. To solve this, you can include an object translating these values. 6 | 7 | ## Usage 8 | 9 | ```js 10 | const a = require('@viget/tailwindcss-plugins/utilities/alpha') 11 | 12 | module.exports = { 13 | theme: { 14 | colors: { 15 | 'purple': '#9f7aea', 16 | 'purple-50': `#9f7aea${a[50]}`, 17 | } 18 | // ... 19 | } 20 | } 21 | ``` -------------------------------------------------------------------------------- /utilities/alpha/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 100: 'ff', 3 | 99: 'fc', 4 | 98: 'fa', 5 | 97: 'f7', 6 | 96: 'f5', 7 | 95: 'f2', 8 | 94: 'f0', 9 | 93: 'ed', 10 | 92: 'eb', 11 | 91: 'e8', 12 | 90: 'e6', 13 | 89: 'e3', 14 | 88: 'e0', 15 | 87: 'de', 16 | 86: 'db', 17 | 85: 'd9', 18 | 84: 'd6', 19 | 83: 'd4', 20 | 82: 'd1', 21 | 81: 'cf', 22 | 80: 'cc', 23 | 79: 'c9', 24 | 78: 'c7', 25 | 77: 'c4', 26 | 76: 'c2', 27 | 75: 'bf', 28 | 74: 'bd', 29 | 73: 'ba', 30 | 72: 'b8', 31 | 71: 'b5', 32 | 70: 'b3', 33 | 69: 'b0', 34 | 68: 'ad', 35 | 67: 'ab', 36 | 66: 'a8', 37 | 65: 'a6', 38 | 64: 'a3', 39 | 63: 'a1', 40 | 62: '9e', 41 | 61: '9c', 42 | 60: '99', 43 | 59: '96', 44 | 58: '94', 45 | 57: '91', 46 | 56: '8f', 47 | 55: '8c', 48 | 54: '8a', 49 | 53: '87', 50 | 52: '85', 51 | 51: '82', 52 | 50: '80', 53 | 49: '7d', 54 | 48: '7a', 55 | 47: '78', 56 | 46: '75', 57 | 45: '73', 58 | 44: '70', 59 | 43: '6e', 60 | 42: '6b', 61 | 41: '69', 62 | 40: '66', 63 | 39: '63', 64 | 38: '61', 65 | 37: '5e', 66 | 36: '5c', 67 | 35: '59', 68 | 34: '57', 69 | 33: '54', 70 | 32: '52', 71 | 31: '4f', 72 | 30: '4d', 73 | 29: '4a', 74 | 28: '47', 75 | 27: '45', 76 | 26: '42', 77 | 25: '40', 78 | 24: '3d', 79 | 23: '3b', 80 | 22: '38', 81 | 21: '36', 82 | 20: '33', 83 | 19: '30', 84 | 18: '2e', 85 | 17: '2b', 86 | 16: '29', 87 | 15: '26', 88 | 14: '24', 89 | 13: '21', 90 | 12: '1f', 91 | 11: '1c', 92 | 10: '1a', 93 | 9: '17', 94 | 8: '14', 95 | 7: '12', 96 | 6: '0f', 97 | 5: '0d', 98 | 4: '0a', 99 | 3: '08', 100 | 2: '05', 101 | 1: '03', 102 | 0: '00', 103 | } 104 | -------------------------------------------------------------------------------- /utilities/fns/README.md: -------------------------------------------------------------------------------- 1 | # functions 2 | 3 | These helpers ease unit conversion and simplify config entries. 4 | 5 | - **em()**: Convert pixel value into ems 6 | - **rem()**: Convert pixel value into rems 7 | - **remPair()**: Convert pixel value to a rem-based object entry intended for spreading 8 | - **pxPair()**: Convert pixel value to a pixel-based object entry intended for spreading 9 | 10 | ## Usage 11 | 12 | ```js 13 | const { em, rem, remPair, pxPair } = require('@viget/tailwindcss-plugins/utilities/fns') 14 | 15 | screens: { 16 | sm: em(640), 17 | }, 18 | width: { 19 | logo: rem(16), 20 | }, 21 | height: { 22 | ...remPair(32), 23 | }, 24 | borderWidth: { 25 | ...pxPair(8), 26 | }, 27 | ``` 28 | 29 | is equivalent to: 30 | 31 | ```js 32 | screens: { 33 | sm: '40em', 34 | }, 35 | width: { 36 | logo: '1rem', 37 | }, 38 | height: { 39 | 32: '2rem', 40 | }, 41 | borderWidth: { 42 | 8: '8px', 43 | }, 44 | ``` 45 | -------------------------------------------------------------------------------- /utilities/fns/index.js: -------------------------------------------------------------------------------- 1 | const rem = (px) => `${px / 16}rem` 2 | const remPair = (px) => ({ [px]: rem(px) }) 3 | const em = (px) => `${px / 16}em` 4 | const pxPair = (px) => ({ [px]: `${px}px` }) 5 | 6 | module.exports = { rem, remPair, em, pxPair } 7 | --------------------------------------------------------------------------------