├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ └── config.yml ├── release-drafter.yml ├── renovate.json └── workflows │ ├── codeql-analysis.yml │ ├── comment-released-prs-and-issues.yml │ ├── draft-release.yml │ ├── npm-publish-dev.yml │ ├── npm-publish.yml │ ├── size.yml │ └── test.yml ├── .gitignore ├── .size-limit.json ├── LICENSE.md ├── README.md ├── TODO.md ├── assets └── logo.svg ├── benchmark ├── _generated │ └── inputs.json ├── shared.ts ├── tailwind-merge-cacheless.ts ├── tailwind-merge.ts ├── tw-merge-cacheless.ts └── tw-merge.ts ├── docs ├── api-reference.md ├── configuration.md ├── contributing.md ├── features.md ├── recipes.md ├── similar-packages.md ├── versioning.md ├── what-is-it-for.md └── writing-plugins.md ├── dts.config.js ├── package.json ├── scripts ├── gen-benchmark-data.ts ├── gen-tailwind-rules.ts ├── test-built-package-exports.js └── test-built-package-exports.mjs ├── src ├── generate-tailwind-rule-set │ ├── data.ts │ ├── gen-file.ts │ ├── gen.ts │ ├── generation-state.ts │ ├── index.ts │ ├── process-utility.ts │ ├── types.ts │ └── utilities-by-category.ts ├── index.ts ├── lib │ ├── create-lru-cache.ts │ ├── create-merge.ts │ └── utils.ts ├── rules.ts └── tailwind.ts ├── tests ├── arbitrary-properties.test.ts ├── arbitrary-values.test.ts ├── arbitrary-variants.test.ts ├── class-group-conflicts.test.ts ├── colors.test.ts ├── conflicts-across-class-groups.test.ts ├── content-utilities.test.ts ├── docs-examples.test.ts ├── important-modifier.test.ts ├── negative-values.test.ts ├── non-conflicting-classes.test.ts ├── non-tailwind-classes.test.ts ├── per-side-border-colors.test.ts ├── prefixes.test.ts ├── pseudo-variants.test.ts ├── separators.test.ts ├── standalone-classes.test.ts ├── tailwind-css-versions.test.ts ├── tsconfig.json └── tw-merge.test.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@typescript-eslint/base", "plugin:import/typescript"], 3 | "plugins": ["import"], 4 | "ignorePatterns": [ 5 | "coverage/**", 6 | "dist/**", 7 | "node_modules/**", 8 | "*.json", 9 | "*.lock", 10 | "*.md", 11 | "*.svg" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/consistent-type-assertions": "warn", 15 | "@typescript-eslint/no-array-constructor": "warn", 16 | "@typescript-eslint/no-redeclare": "warn", 17 | "@typescript-eslint/no-unused-expressions": [ 18 | "error", 19 | { 20 | "allowShortCircuit": true, 21 | "allowTaggedTemplates": true, 22 | "allowTernary": true 23 | } 24 | ], 25 | "@typescript-eslint/no-unused-vars": [ 26 | "warn", 27 | { 28 | "args": "none", 29 | "ignoreRestSiblings": true 30 | } 31 | ], 32 | "@typescript-eslint/no-use-before-define": [ 33 | "warn", 34 | { 35 | "classes": false, 36 | "functions": false, 37 | "typedefs": false, 38 | "variables": false 39 | } 40 | ], 41 | "@typescript-eslint/no-useless-constructor": "warn", 42 | "array-callback-return": "warn", 43 | "dot-location": ["warn", "property"], 44 | "eqeqeq": ["warn", "smart"], 45 | "getter-return": "warn", 46 | "import/first": "error", 47 | "import/no-default-export": "warn", 48 | "import/no-named-as-default": "warn", 49 | "import/no-named-as-default-member": "warn", 50 | "import/no-duplicates": "warn", 51 | "import/order": [ 52 | "warn", 53 | { 54 | "groups": [ 55 | "builtin", 56 | "external", 57 | "internal", 58 | "parent", 59 | "sibling", 60 | "index" 61 | ], 62 | "newlines-between": "always" 63 | } 64 | ], 65 | "new-parens": "warn", 66 | "no-caller": "warn", 67 | "no-cond-assign": ["warn", "except-parens"], 68 | "no-console": "warn", 69 | "no-const-assign": "warn", 70 | "no-control-regex": "warn", 71 | "no-delete-var": "warn", 72 | "no-dupe-args": "warn", 73 | "no-dupe-keys": "warn", 74 | "no-duplicate-case": "warn", 75 | "no-empty-character-class": "warn", 76 | "no-empty-pattern": "warn", 77 | "no-eval": "warn", 78 | "no-ex-assign": "warn", 79 | "no-extend-native": "warn", 80 | "no-extra-bind": "warn", 81 | "no-extra-label": "warn", 82 | "no-fallthrough": "warn", 83 | "no-func-assign": "warn", 84 | "no-implied-eval": "warn", 85 | "no-invalid-regexp": "warn", 86 | "no-iterator": "warn", 87 | "no-label-var": "warn", 88 | "no-labels": [ 89 | "warn", 90 | { 91 | "allowLoop": true, 92 | "allowSwitch": false 93 | } 94 | ], 95 | "no-lone-blocks": "warn", 96 | "no-loop-func": "warn", 97 | "no-mixed-operators": [ 98 | "warn", 99 | { 100 | "allowSamePrecedence": false, 101 | "groups": [ 102 | ["&", "|", "^", "~", "<<", ">>", ">>>"], 103 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 104 | ["&&", "||"], 105 | ["in", "instanceof"] 106 | ] 107 | } 108 | ], 109 | "no-multi-str": "warn", 110 | "no-native-reassign": "warn", 111 | "no-negated-in-lhs": "warn", 112 | "no-new-func": "warn", 113 | "no-new-object": "warn", 114 | "no-new-symbol": "warn", 115 | "no-new-wrappers": "warn", 116 | "no-obj-calls": "warn", 117 | "no-octal": "warn", 118 | "no-octal-escape": "warn", 119 | "no-regex-spaces": "warn", 120 | "no-restricted-properties": [ 121 | "error", 122 | { 123 | "message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting", 124 | "object": "require", 125 | "property": "ensure" 126 | }, 127 | { 128 | "message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting", 129 | "object": "System", 130 | "property": "import" 131 | } 132 | ], 133 | "no-restricted-syntax": ["warn", "WithStatement"], 134 | "no-script-url": "warn", 135 | "no-self-assign": "warn", 136 | "no-self-compare": "warn", 137 | "no-sequences": "warn", 138 | "no-shadow-restricted-names": "warn", 139 | "no-sparse-arrays": "warn", 140 | "no-template-curly-in-string": "warn", 141 | "no-this-before-super": "warn", 142 | "no-throw-literal": "warn", 143 | "no-unreachable": "warn", 144 | "no-unused-labels": "warn", 145 | "no-useless-computed-key": "warn", 146 | "no-useless-concat": "warn", 147 | "no-useless-escape": "warn", 148 | "no-useless-rename": [ 149 | "warn", 150 | { 151 | "ignoreDestructuring": false, 152 | "ignoreExport": false, 153 | "ignoreImport": false 154 | } 155 | ], 156 | "no-whitespace-before-property": "warn", 157 | "no-with": "warn", 158 | "require-yield": "warn", 159 | "rest-spread-spacing": ["warn", "never"], 160 | "strict": ["warn", "never"], 161 | "unicode-bom": ["warn", "never"], 162 | "use-isnan": "warn", 163 | "valid-typeof": "warn" 164 | }, 165 | "overrides": [ 166 | { 167 | "files": ["tests/**/*.ts"], 168 | "extends": ["plugin:jest/recommended", "plugin:jest/style"] 169 | }, 170 | { 171 | "files": ["scripts/**/*.?(m)js"], 172 | "rules": { 173 | "no-console": "off" 174 | } 175 | } 176 | ] 177 | } 178 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # tailwind-merge Community Guidelines 2 | 3 | The following community guidelines are based on [The Ruby Community Conduct Guidelines](https://www.ruby-lang.org/en/conduct) which are also used by [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss/blob/master/.github/CODE_OF_CONDUCT.md). 4 | 5 | This document provides community guidelines for a safe, respectful, productive, and collaborative place for any person who is willing to contribute to the tailwind-merge project. It applies to all “collaborative space”, which is defined as community communications channels (such as issues, discussions, pull requests, commit comments, etc.). 6 | 7 | - Participants will be tolerant of opposing views. 8 | - Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. 9 | - When interpreting the words and actions of others, participants should always assume good intentions. 10 | - Behaviour which can be reasonably considered harassment will not be tolerated. 11 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Please take a moment to review this document before creating an issue or pull request. It is based on the [Tiptap contributing guidelines](https://github.com/ueberdosis/tiptap/blob/main/CONTRIBUTING.md). 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. 10 | 11 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. 12 | 13 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 14 | 15 | See [CODE OF CONDUCT](CODE_OF_CONDUCT.md) for more info. 16 | 17 | ## Viability 18 | 19 | When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. 20 | 21 | ## Procedure 22 | 23 | Before filing an issue: 24 | 25 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 26 | - Check to make sure your feature suggestion isn't already present within the project. 27 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 28 | - Check the pull requests tab to ensure that the feature isn't already in progress. 29 | 30 | Before submitting a pull request: 31 | 32 | - Check the codebase to ensure that your feature doesn't already exist. 33 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 34 | 35 | ## Requirements 36 | 37 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 38 | 39 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 40 | 41 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 42 | 43 | ## Development 44 | 45 | You will need [Node.js](https://nodejs.org) and [yarn](https://classic.yarnpkg.com) installed on your machine. I recommend running tests in watch mode while you work on the code. Then the correct subset of tests is being run as you modify source code or the tests itself. 46 | 47 | ```sh 48 | # Install dependencies 49 | $ yarn 50 | # Run tests 51 | $ yarn test --watch 52 | ``` 53 | 54 | Happy coding! 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Get help 4 | url: https://github.com/compi-ui/tw-merge/discussions/new?category=help 5 | about: If you can't get something to work the way you expect, open a question in the discussion forums. 6 | - name: Feature request 7 | url: https://github.com/compi-ui/tw-merge/discussions/new?category=ideas 8 | about: Suggest any ideas you have using the discussion forum. 9 | - name: Bug report 10 | url: https://github.com/compi-ui/tw-merge/issues/new 11 | about: If something is broken with tw-merge itself, create a bug report. 12 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | template: | 4 | $CHANGES 5 | 6 | **Full Changelog**: https://github.com/compi-ui/tw-merge/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 7 | category-template: "### $TITLE" 8 | change-template: "- $TITLE by @$AUTHOR in https://github.com/compi-ui/tw-merge/pull/$NUMBER" 9 | change-title-escapes: '\<*_&' 10 | no-changes-template: "No changes" 11 | categories: 12 | - title: "⚠️ Needs Changelog Edit" 13 | label: "needs changelog edit" 14 | - title: "Breaking Changes" 15 | label: "breaking" 16 | - title: "New Features" 17 | label: "feature" 18 | - title: "Bug Fixes" 19 | label: "bugfix" 20 | - title: "Other" 21 | label: "other" 22 | exclude-labels: 23 | - "skip changelog" 24 | version-resolver: 25 | major: 26 | labels: 27 | - "breaking" 28 | minor: 29 | labels: 30 | - "feature" 31 | patch: 32 | labels: 33 | - "bugfix" 34 | - "other" 35 | default: "patch" 36 | autolabeler: 37 | - label: "feature" 38 | branch: 39 | - '/\bfeature\b/i' 40 | title: 41 | - '/\bfeature\b/i' 42 | - label: "bugfix" 43 | branch: 44 | - '/\b(bugfix|fix)\b/i' 45 | title: 46 | - '/\b(bugfix|fix)\b/i' 47 | - label: "other" 48 | branch: 49 | - '/^other\b/i' 50 | - label: "breaking" 51 | branch: 52 | - "/^breaking-/i" 53 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":combinePatchMinorReleases", 5 | "group:allNonMajor", 6 | ":labels(dependencies,skip changelog)", 7 | ":assignee(DaniGuardiola)", 8 | ":enableVulnerabilityAlertsWithLabel(security)", 9 | "schedule:monthly" 10 | ], 11 | "timezone": "Europe/Berlin", 12 | "prHourlyLimit": 10, 13 | "rangeStrategy": "bump" 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | schedule: 7 | - cron: "30 8 * * 5" 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | - name: Use node_modules cache 22 | uses: actions/cache@v3 23 | with: 24 | path: node_modules 25 | key: yarn-node-16-lock-${{ hashFiles('yarn.lock') }} 26 | restore-keys: | 27 | yarn-node-16-lock- 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v2 30 | with: 31 | languages: javascript 32 | - run: yarn install --frozen-lockfile 33 | - run: yarn build 34 | - name: Perform CodeQL Analysis 35 | uses: github/codeql-action/analyze@v2 36 | -------------------------------------------------------------------------------- /.github/workflows/comment-released-prs-and-issues.yml: -------------------------------------------------------------------------------- 1 | name: Comment released PRs and issues 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: apexskier/github-release-commenter@v1 15 | with: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | comment-template: This was addressed in release {release_link}. 18 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Draft release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, reopened, synchronize] 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | permissions: write-all 17 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-dev.yml: -------------------------------------------------------------------------------- 1 | name: npm publish dev 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Use Node.js 16 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | # More info: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry 19 | registry-url: "https://registry.npmjs.org" 20 | - name: Use node_modules cache 21 | uses: actions/cache@v3 22 | with: 23 | path: node_modules 24 | key: yarn-node-16-lock-${{ hashFiles('yarn.lock') }} 25 | restore-keys: | 26 | yarn-node-16-lock- 27 | - run: yarn install --frozen-lockfile 28 | - run: yarn build 29 | - uses: actions/upload-artifact@v3 30 | with: 31 | name: build-output 32 | path: dist 33 | if-no-files-found: error 34 | - run: yarn size 35 | - uses: martinbeentjes/npm-get-version-action@v1.3.1 36 | id: package-version 37 | - run: yarn version --no-git-tag-version --new-version ${{ steps.package-version.outputs.current-version }}-dev.${{ github.sha }} 38 | # npm install -g npm@latest is necessary to make provenance available. More info: https://docs.npmjs.com/generating-provenance-statements 39 | - run: npm install -g npm@latest 40 | - run: npm publish --access public --tag dev 41 | env: 42 | # Is connected with actions/setup-node -> registry-url 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Use Node.js 16 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - name: Use node_modules cache 19 | uses: actions/cache@v3 20 | with: 21 | path: node_modules 22 | key: yarn-node-16-lock-${{ hashFiles('yarn.lock') }} 23 | restore-keys: | 24 | yarn-node-16-lock- 25 | - run: yarn install --frozen-lockfile 26 | - run: yarn lint 27 | - run: yarn test --ci --coverage --maxWorkers=2 28 | - run: yarn build 29 | - uses: actions/upload-artifact@v3 30 | with: 31 | name: build-output 32 | path: dist 33 | if-no-files-found: error 34 | - run: yarn test:exports 35 | - run: yarn size 36 | # npm install -g npm@latest is necessary to make provenance available. More info: https://docs.npmjs.com/generating-provenance-statements 37 | - run: npm install -g npm@latest 38 | - uses: JS-DevTools/npm-publish@v1 39 | with: 40 | token: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | size: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | pull-requests: write 10 | env: 11 | CI_JOB_NUMBER: 1 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: andresz1/size-limit-action@v1 15 | with: 16 | github_token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ["**"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Use Node.js 16 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - name: Use node_modules cache 19 | uses: actions/cache@v3 20 | with: 21 | path: node_modules 22 | key: yarn-node-16-lock-${{ hashFiles('yarn.lock') }} 23 | restore-keys: | 24 | yarn-node-16-lock- 25 | - run: yarn install --frozen-lockfile 26 | - run: yarn lint 27 | - run: yarn test --ci --coverage --maxWorkers=2 28 | - run: yarn build 29 | - uses: actions/upload-artifact@v3 30 | with: 31 | name: build-output 32 | path: dist 33 | if-no-files-found: error 34 | - run: yarn test:exports 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | /dist/ 3 | coverage 4 | node_modules/ 5 | .DS_Store 6 | *.local 7 | .idea -------------------------------------------------------------------------------- /.size-limit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "dist/tw-merge.mjs", 4 | "limit": "10 KB" 5 | }, 6 | { 7 | "path": "dist/tw-merge.cjs.production.min.js", 8 | "limit": "10 KB" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dani Guardiola, Dany Castillo 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 | # IMPORTANT NOTE: this package is a fork in development and ALL of the documentation has not been updated yet 2 | 3 |
4 |
5 | 6 | 7 | tailwind-merge 8 | 9 | 10 |
11 | 12 | # tailwind-merge 13 | 14 | Utility function to efficiently merge [Tailwind CSS](https://tailwindcss.com) classes in JS without style conflicts. 15 | 16 | ```ts 17 | import { twMerge } from "tailwind-merge"; 18 | 19 | twMerge("px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]"); 20 | // → 'hover:bg-dark-red p-3 bg-[#B91C1C]' 21 | ``` 22 | 23 | - Supports Tailwind v3.0 up to v3.3 (if you use Tailwind v2, use [tailwind-merge v0.9.0](https://github.com/compi-ui/tw-merge/tree/v0.9.0)) 24 | - Works in all modern browsers and Node >=12 25 | - Fully typed 26 | - [Check bundle size on Bundlephobia](https://bundlephobia.com/package/tailwind-merge) 27 | 28 | ## Get started 29 | 30 | 31 | 32 | - [What is it for](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/what-is-it-for.md) 33 | - [Features](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/features.md) 34 | - [Configuration](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/configuration.md) 35 | - [Recipes](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/recipes.md) 36 | - [API reference](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/api-reference.md) 37 | - [Writing plugins](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/writing-plugins.md) 38 | - [Versioning](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/versioning.md) 39 | - [Contributing](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/.github/CONTRIBUTING.md) 40 | - [Similar packages](https://github.com/compi-ui/tw-merge/tree/v0.0.1-alpha.0/docs/similar-packages.md) 41 | 42 | 43 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Tailwind ruleset 2 | 3 | - [x] prefix 4 | - [x] separator 5 | - [x] ruleset generator from Tailwind theme 6 | - [ ] complete ruleset generator 7 | - [ ] add smaller tailwind rulesets 8 | - [ ] add remaining rules 9 | - [ ] add filesystem target 10 | 11 | # Testing and benchmarking 12 | 13 | - [x] enable prefixes and separators tests 14 | - [ ] create comparison benchmark with tailwing-merge 15 | - [ ] create "test creator" for existing tailwing-merge and utility merge 16 | 17 | # Chores 18 | 19 | - [ ] update docs 20 | - [ ] update README 21 | - [ ] update other docs (e.g. CONTRIBUTING.md) 22 | - [ ] figure out the license 23 | - [ ] JSDoc in everything 24 | 25 | # Improvement opportunities 26 | 27 | - [x] always default 28 | - [x] always trailing slash 29 | - [x] `simpleRules`: instead of creating a rule for each target, do a single one with a mega-regex 30 | - [x] universal cardinal rule setup 31 | 32 | # Publishing 33 | 34 | - [ ] update publishing scripts 35 | - [ ] publish 36 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | tailwind-merge 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /benchmark/_generated/inputs.json: -------------------------------------------------------------------------------- 1 | [ 2 | "static sticky relative", 3 | "p-5 p-2 p-4", 4 | "p-3 px-5", 5 | "inset-x-4 right-4", 6 | "inset-x-px -inset-1", 7 | "bottom-auto inset-y-6", 8 | "inline block", 9 | "p-2 hover:p-4", 10 | "hover:p-2 hover:p-4", 11 | "hover:focus:p-2 focus:hover:p-4", 12 | "bg-black bg-[color:var(--mystery-var)]", 13 | "grid-cols-[1fr,auto] grid-cols-2", 14 | "[mask-type:luminance] [mask-type:alpha]", 15 | "[--scroll-offset:56px] lg:[--scroll-offset:44px]", 16 | "[padding:1rem] p-8", 17 | "[&:nth-child(3)]:py-0 [&:nth-child(3)]:py-4", 18 | "dark:hover:[&:nth-child(3)]:py-0 hover:dark:[&:nth-child(3)]:py-4", 19 | "[&:focus]:ring focus:ring-4", 20 | "!p-3 !p-4 p-5", 21 | "!right-2 !-inset-x-1", 22 | "text-sm leading-6 text-lg/7", 23 | "p-5 p-2 my-non-tailwind-class p-4", 24 | "text-red text-secret-sauce", 25 | "border rounded px-2 py-1", 26 | "[paint-order:markers] [paint-order:normal]", 27 | "[paint-order:markers] hover:[paint-order:normal]", 28 | "hover:[paint-order:markers] hover:[paint-order:normal]", 29 | "[-unknown-prop:::123:::] [-unknown-prop:url(https://hi.com)]", 30 | "![some:prop] [some:other]", 31 | "![some:prop] [some:other] [some:one] ![some:another]", 32 | "m-[2px] m-[10px]\")).toBe(\"m-[10px]", 33 | "z-20 z-[99]\")).toBe(\"z-[99]", 34 | "my-[2px] m-[10rem]\")).toBe(\"m-[10rem]", 35 | "cursor-pointer cursor-[grab]\")).toBe(\"cursor-[grab]", 36 | "m-[2px] m-[calc(100%-var(--arbitrary))]", 37 | "m-[2px] m-[length:var(--mystery-var)]", 38 | "opacity-10 opacity-[0.025]\")).toBe(\"opacity-[0.025]", 39 | "scale-75 scale-[1.7]\")).toBe(\"scale-[1.7]", 40 | "brightness-90 brightness-[1.75]\")).toBe(\"brightness-[1.75]", 41 | "hover:m-[2px] hover:m-[length:var(--c)]", 42 | "hover:focus:m-[2px] focus:hover:m-[length:var(--c)]", 43 | "border-b border-[color:rgb(var(--color-gray-500-rgb)/50%))]", 44 | "border-[color:rgb(var(--color-gray-500-rgb)/50%))] border-b", 45 | "grid-rows-[1fr,auto] grid-rows-2\")).toBe(\"grid-rows-2", 46 | "grid-rows-[repeat(20,minmax(0,1fr))] grid-rows-3", 47 | "[&>*]:underline [&>*]:line-through", 48 | "[&>*]:underline [&>*]:line-through [&_div]:line-through", 49 | "supports-[display:grid]:flex supports-[display:grid]:grid", 50 | "dark:lg:hover:[&>*]:underline dark:lg:hover:[&>*]:line-through", 51 | "dark:lg:hover:[&>*]:underline dark:hover:lg:[&>*]:line-through", 52 | "hover:[&>*]:underline [&>*]:hover:line-through", 53 | "[&[data-open]]:underline [&[data-open]]:line-through", 54 | "[&>*]:[&_div]:underline [&>*]:[&_div]:line-through", 55 | "[&>*]:[&_div]:underline [&_div]:[&>*]:line-through", 56 | "[&>*]:[color:red] [&>*]:[color:blue]", 57 | "overflow-x-auto overflow-x-hidden", 58 | "w-full w-fit\")).toBe(\"w-fit", 59 | "overflow-x-auto overflow-x-hidden overflow-x-scroll", 60 | "overflow-x-auto hover:overflow-x-hidden overflow-x-scroll", 61 | "lining-nums tabular-nums diagonal-fractions", 62 | "normal-nums tabular-nums diagonal-fractions", 63 | "tabular-nums diagonal-fractions normal-nums", 64 | "tabular-nums proportional-nums\")).toBe(\"proportional-nums", 65 | "bg-grey-5 bg-hotpink\")).toBe(\"bg-hotpink", 66 | "hover:bg-grey-5 hover:bg-hotpink\")).toBe(\"hover:bg-hotpink", 67 | "inset-1 inset-x-1\")).toBe(\"inset-1 inset-x-1", 68 | "inset-x-1 inset-1\")).toBe(\"inset-1", 69 | "inset-x-1 left-1 inset-1\")).toBe(\"inset-1", 70 | "inset-x-1 inset-1 left-1\")).toBe(\"inset-1 left-1", 71 | "inset-x-1 right-1 inset-1\")).toBe(\"inset-1", 72 | "inset-x-1 right-1 inset-x-1\")).toBe(\"inset-x-1", 73 | "inset-x-1 right-1 inset-y-1", 74 | "right-1 inset-x-1 inset-y-1\")).toBe(\"inset-x-1 inset-y-1", 75 | "inset-x-1 hover:left-1 inset-1", 76 | "ring shadow\")).toBe(\"ring shadow", 77 | "ring-2 shadow-md\")).toBe(\"ring-2 shadow-md", 78 | "shadow ring\")).toBe(\"shadow ring", 79 | "shadow-md ring-2\")).toBe(\"shadow-md ring-2", 80 | "content-['hello'] content-[attr(data-content)]", 81 | "!font-medium !font-bold\")).toBe(\"!font-bold", 82 | "!font-medium !font-bold font-thin", 83 | "!right-2 !-inset-x-px\")).toBe(\"!-inset-x-px", 84 | "focus:!inline focus:!block\")).toBe(\"focus:!block", 85 | "-m-2 -m-5\")).toBe(\"-m-5", 86 | "-top-12 -top-2000\")).toBe(\"-top-2000", 87 | "-m-2 m-auto\")).toBe(\"m-auto", 88 | "top-12 -top-69\")).toBe(\"-top-69", 89 | "-right-1 inset-x-1\")).toBe(\"inset-x-1", 90 | "hover:focus:-right-1 focus:hover:inset-x-1", 91 | "border-t border-white/10\")).toBe(\"border-t border-white/10", 92 | "border-t border-white\")).toBe(\"border-t border-white", 93 | "text-3.5xl text-black\")).toBe(\"text-3.5xl text-black", 94 | "non-tailwind-class inline block", 95 | "inline block inline-1\")).toBe(\"block inline-1", 96 | "inline block i-inline\")).toBe(\"block i-inline", 97 | "focus:inline focus:block focus:inline-1", 98 | "border-t-some-blue border-t-other-blue", 99 | "border-t-some-blue border-some-blue", 100 | "tw-block tw-hidden\")).toBe(\"tw-hidden", 101 | "block hidden\")).toBe(\"block hidden", 102 | "tw-p-3 tw-p-2\")).toBe(\"tw-p-2", 103 | "p-3 p-2\")).toBe(\"p-3 p-2", 104 | "!tw-right-0 !tw-inset-0\")).toBe(\"!tw-inset-0", 105 | "hover:focus:!tw-right-0 focus:hover:!tw-inset-0", 106 | "empty:p-2 empty:p-3\")).toBe(\"empty:p-3", 107 | "hover:empty:p-2 hover:empty:p-3\")).toBe(\"hover:empty:p-3", 108 | "read-only:p-2 read-only:p-3\")).toBe(\"read-only:p-3", 109 | "group-empty:p-2 group-empty:p-3\")).toBe(\"group-empty:p-3", 110 | "peer-empty:p-2 peer-empty:p-3\")).toBe(\"peer-empty:p-3", 111 | "group-empty:p-2 peer-empty:p-3", 112 | "hover:group-empty:p-2 hover:group-empty:p-3", 113 | "group-read-only:p-2 group-read-only:p-3", 114 | "block hidden\")).toBe(\"hidden", 115 | "p-3 p-2\")).toBe(\"p-2", 116 | "!right-0 !inset-0\")).toBe(\"!inset-0", 117 | "hover_focus_!right-0 focus_hover_!inset-0", 118 | "hover:focus:!right-0 focus:hover:!inset-0", 119 | "block hidden\")).toBe(\"hidden", 120 | "p-3 p-2\")).toBe(\"p-2", 121 | "!right-0 !inset-0\")).toBe(\"!inset-0", 122 | "hover__focus__!right-0 focus__hover__!inset-0", 123 | "hover:focus:!right-0 focus:hover:!inset-0", 124 | "inline block\")).toBe(\"block", 125 | "hover:block hover:inline\")).toBe(\"hover:inline", 126 | "hover:block hover:block\")).toBe(\"hover:block", 127 | "inline hover:inline focus:inline hover:block hover:focus:block", 128 | "underline line-through\")).toBe(\"line-through", 129 | "line-through no-underline\")).toBe(\"no-underline", 130 | "text-red text-lg/7 text-lg/8\")).toBe(\"text-red text-lg/8", 131 | "hyphens-auto hyphens-manual\")).toBe(\"hyphens-manual", 132 | "from-0% from-red\")).toBe(\"from-0% from-red", 133 | "caption-top caption-bottom\")).toBe(\"caption-bottom", 134 | "line-clamp-2 line-clamp-none line-clamp-[10]", 135 | "delay-150 delay-0 duration-150 duration-0", 136 | "justify-normal justify-center justify-stretch", 137 | "content-normal content-center content-stretch", 138 | "whitespace-nowrap whitespace-break-spaces", 139 | "mix-blend-normal mix-blend-multiply", 140 | "h-10 h-min\")).toBe(\"h-min", 141 | "stroke-black stroke-1\")).toBe(\"stroke-black stroke-1", 142 | "stroke-2 stroke-[3]\")).toBe(\"stroke-[3]", 143 | "outline outline-black outline-1", 144 | "outline outline-dashed\")).toBe(\"outline-dashed", 145 | "outline-dashed outline-black outline", 146 | "grayscale-0 grayscale-[50%]\")).toBe(\"grayscale-[50%]", 147 | "grow grow-[2]\")).toBe(\"grow-[2]", 148 | "bg-blue-500 bg-gradient-to-t bg-origin-top bg-none bg-red-500", 149 | "bg-blue-500 bg-none bg-origin-top bg-gradient-to-t", 150 | "supports-[display:grid]:grid hover:p-4 !font-medium inset-x-1 inset-x-1 !-inset-x-1 [&>*]:[&_div]:underline supports-[display:grid]:grid focus:!block\")).toBe(\"focus:!block inset-y-1\")).toBe(\"inset-x-1", 151 | "cursor-[grab]\")).toBe(\"cursor-[grab] relative border-t-some-blue [&[data-open]]:line-through m-[2px] h-min\")).toBe(\"h-min py-1 !tw-right-0 hover:[&>*]:underline inset-x-1", 152 | "whitespace-nowrap overflow-x-hidden focus:hover:!tw-inset-0 text-red dark:lg:hover:[&>*]:underline tabular-nums stroke-1 bg-red-500 p-3 [some:other]", 153 | "grid-rows-3 inline left-1\")).toBe(\"inset-1 hover:p-2 no-underline\")).toBe(\"no-underline !-inset-x-1 !tw-right-0 outline from-red overflow-x-auto", 154 | "[&>*]:[&_div]:underline rounded grid-rows-3 focus:inline m-[10px]\")).toBe(\"m-[10px] group-empty:p-2 line-through tabular-nums !right-0 p-3", 155 | "-top-12 hover:m-[length:var(--c)] hover:block [--scroll-offset:56px] hover:focus:-right-1 z-[99]\")).toBe(\"z-[99] grow-[2]\")).toBe(\"grow-[2] focus:inline hyphens-auto hover:focus:!tw-right-0", 156 | "delay-0 inset-x-1\")).toBe(\"inset-1 hover:m-[2px] border-b inset-x-1 inline hover:p-4 bg-gradient-to-t outline peer-empty:p-3\")).toBe(\"peer-empty:p-3", 157 | "supports-[display:grid]:grid hover:overflow-x-hidden rounded p-2\")).toBe(\"p-3 inline ![some:another] focus:block !font-medium text-lg/8 right-1", 158 | "!right-0 p-3 p-2 bg-none h-10 border-white\")).toBe(\"border-t border-some-blue block bg-grey-5 block", 159 | "inset-y-1\")).toBe(\"inset-x-1 group-empty:p-3\")).toBe(\"group-empty:p-3 border outline hidden\")).toBe(\"hidden left-1 m-[2px] inset-x-1\")).toBe(\"inset-1 my-non-tailwind-class scale-[1.7]\")).toBe(\"scale-[1.7]", 160 | "border-t left-1\")).toBe(\"inset-1 justify-normal focus__hover__!inset-0 m-[length:var(--mystery-var)] no-underline\")).toBe(\"no-underline block\")).toBe(\"block z-[99]\")).toBe(\"z-[99] [&>*]:[&_div]:underline hover:[&>*]:underline", 161 | "hover:focus:!tw-right-0 right-4 block\")).toBe(\"block inline [&>*]:line-through ring [paint-order:markers] outline diagonal-fractions m-[2px]", 162 | "shadow\")).toBe(\"ring border-t-some-blue !-inset-x-1 empty:p-2 inline-1\")).toBe(\"block block h-10 [-unknown-prop:url(https://hi.com)] [mask-type:luminance] outline", 163 | "shadow read-only:p-3\")).toBe(\"read-only:p-3 block !tw-right-0 group-read-only:p-3 h-min\")).toBe(\"h-min border-white/10 !tw-inset-0\")).toBe(\"!tw-inset-0 [&>*]:[&_div]:line-through [&>*]:hover:line-through", 164 | "overflow-x-auto justify-stretch p-2 [&>*]:underline hover:focus:p-2 [some:other] p-5 outline-black p-3 [paint-order:markers]", 165 | "opacity-10 read-only:p-2 hover:focus:block [&:nth-child(3)]:py-4 hover:group-empty:p-3 content-stretch border-white/10\")).toBe(\"border-t z-[99]\")).toBe(\"z-[99] supports-[display:grid]:grid stroke-[3]\")).toBe(\"stroke-[3]", 166 | "border-b text-black\")).toBe(\"text-3.5xl [&>*]:line-through p-2 normal-nums [&_div]:line-through top-12 inline p-2 p-3", 167 | "overflow-x-auto leading-6 content-['hello'] grid-rows-2\")).toBe(\"grid-rows-2 border-t-some-blue hover:block inset-1 font-thin line-through\")).toBe(\"line-through ring", 168 | "relative group-read-only:p-2 !font-medium p-2 hover:group-empty:p-2 dark:hover:[&:nth-child(3)]:py-0 stroke-1 !right-2 hidden\")).toBe(\"block sticky", 169 | "hidden\")).toBe(\"hidden shadow-md line-through\")).toBe(\"line-through hover:bg-hotpink\")).toBe(\"hover:bg-hotpink !right-0 mix-blend-normal outline inset-y-6 group-empty:p-2 dark:hover:lg:[&>*]:line-through", 170 | "inset-x-1 hover:focus:!right-0 hover__focus__!right-0 group-empty:p-2 px-5 hidden\")).toBe(\"hidden bg-blue-500 from-red\")).toBe(\"from-0% ring my-non-tailwind-class", 171 | "inset-1 from-red scale-75 group-empty:p-3\")).toBe(\"group-empty:p-3 !right-2 outline-black inset-x-4 grayscale-[50%]\")).toBe(\"grayscale-[50%] p-4 !inset-0\")).toBe(\"!inset-0", 172 | "tw-block tw-hidden\")).toBe(\"tw-hidden inset-x-px grid-rows-[repeat(20,minmax(0,1fr))] p-4 bg-none diagonal-fractions hover:focus:!right-0 bg-blue-500 focus:inline", 173 | "brightness-[1.75]\")).toBe(\"brightness-[1.75] focus_hover_!inset-0 inline p-3 hover:[&>*]:underline hover:[paint-order:markers] outline grid-cols-[1fr,auto] relative peer-empty:p-2", 174 | "inline-1\")).toBe(\"block -top-2000\")).toBe(\"-top-2000 stroke-black hover:block h-10 hover:m-[2px] shadow-md focus:inline-1 [&:focus]:ring opacity-10", 175 | "border-[color:rgb(var(--color-gray-500-rgb)/50%))] inset-x-1 ring\")).toBe(\"shadow line-clamp-none diagonal-fractions ring-2 ring [&>*]:[&_div]:underline [some:other] ![some:another]", 176 | "grid-rows-[1fr,auto] !p-4 from-0% p-2\")).toBe(\"p-3 tw-p-3 ![some:prop] mix-blend-normal focus:hover:m-[length:var(--c)] m-[2px] hover:empty:p-2", 177 | "ring scale-75 [some:other] inset-y-1\")).toBe(\"inset-x-1 outline text-black hover:group-empty:p-3 [&>*]:line-through read-only:p-2 z-[99]\")).toBe(\"z-[99]", 178 | "block [&:focus]:ring content-stretch duration-0 -inset-1 -top-2000\")).toBe(\"-top-2000 border-white hover:focus:block static outline", 179 | "[&>*]:hover:line-through bg-gradient-to-t focus:hover:!inset-0 group-empty:p-2 hyphens-auto m-[length:var(--mystery-var)] focus:!inline focus:hover:!inset-0 grow-[2]\")).toBe(\"grow-[2] lining-nums", 180 | "bg-black inset-1 !right-0 stroke-[3]\")).toBe(\"stroke-[3] inset-x-1\")).toBe(\"inset-x-1 duration-0 hidden content-center tabular-nums inline", 181 | "p-2\")).toBe(\"p-2 inset-1 caption-bottom\")).toBe(\"caption-bottom grid-cols-[1fr,auto] inset-x-1\")).toBe(\"inset-1 w-fit\")).toBe(\"w-fit m-[length:var(--mystery-var)] line-through\")).toBe(\"line-through peer-empty:p-2 stroke-2", 182 | "border-some-blue tw-p-3 leading-6 bg-gradient-to-t inline relative diagonal-fractions stroke-2 hover:focus:!tw-right-0 m-[10rem]\")).toBe(\"m-[10rem]", 183 | "content-['hello'] lg:[--scroll-offset:44px] m-auto\")).toBe(\"m-auto shadow-md read-only:p-2 hover:group-empty:p-3 grayscale-0 stroke-black bg-grey-5 hover:focus:p-2", 184 | "i-inline\")).toBe(\"block [&[data-open]]:underline diagonal-fractions empty:p-3\")).toBe(\"empty:p-3 proportional-nums\")).toBe(\"proportional-nums hover:[paint-order:markers] -m-2 inline-1 z-20 inline-1\")).toBe(\"block", 185 | "from-red\")).toBe(\"from-0% hover:p-4 block\")).toBe(\"block brightness-[1.75]\")).toBe(\"brightness-[1.75] inset-x-1 h-10 stroke-[3]\")).toBe(\"stroke-[3] m-[10px]\")).toBe(\"m-[10px] top-12 bg-gradient-to-t", 186 | "hover:[paint-order:markers] empty:p-3\")).toBe(\"empty:p-3 overflow-x-hidden bg-black cursor-[grab]\")).toBe(\"cursor-[grab] hover:focus:-right-1 ![some:prop] -m-5\")).toBe(\"-m-5 hover:empty:p-3\")).toBe(\"hover:empty:p-3 scale-75", 187 | "grid-rows-2\")).toBe(\"grid-rows-2 hover:p-2 bg-blue-500 justify-stretch shadow stroke-[3]\")).toBe(\"stroke-[3] !p-3 p-3 text-3.5xl shadow\")).toBe(\"ring", 188 | "hover:[paint-order:markers] inset-x-1 grid-rows-3 block !inset-0\")).toBe(\"!inset-0 [&:focus]:ring right-1 [&[data-open]]:line-through hover:m-[2px] text-red", 189 | "bg-hotpink\")).toBe(\"bg-hotpink block\")).toBe(\"block scale-[1.7]\")).toBe(\"scale-[1.7] inline border-t-some-blue inset-x-1 hover:dark:[&:nth-child(3)]:py-4 left-1 text-sm h-min\")).toBe(\"h-min", 190 | "caption-top border-white/10\")).toBe(\"border-t font-thin hover:inline\")).toBe(\"hover:inline inset-x-1 h-10 focus:hover:inset-x-1 hover:focus:!right-0 text-lg/7 supports-[display:grid]:grid", 191 | "ring-2\")).toBe(\"shadow-md [padding:1rem] bg-red-500 p-2 left-1 overflow-x-hidden bg-red-500 focus:inline-1 hover:p-4 tabular-nums", 192 | "inline-1\")).toBe(\"block hover:focus:!tw-right-0 left-1\")).toBe(\"inset-1 !font-bold\")).toBe(\"!font-bold sticky hover:[paint-order:markers] w-full from-0% shadow peer-empty:p-3\")).toBe(\"peer-empty:p-3", 193 | "hover:focus:m-[2px] duration-0 inset-x-1 group-empty:p-2 [mask-type:luminance] !font-bold\")).toBe(\"!font-bold hover:p-2 text-lg/7 inset-x-1 hover:[&>*]:underline", 194 | "hover:m-[2px] tw-p-3 px-2 inset-x-4 outline-black outline-black hover:empty:p-3\")).toBe(\"hover:empty:p-3 content-normal p-2\")).toBe(\"p-3 hover:empty:p-2", 195 | "hover:p-2 border-b inset-y-1\")).toBe(\"inset-x-1 grow-[2]\")).toBe(\"grow-[2] left-1 opacity-[0.025]\")).toBe(\"opacity-[0.025] outline [&>*]:[&_div]:underline hidden bg-gradient-to-t", 196 | "bg-black grid-rows-2\")).toBe(\"grid-rows-2 dark:lg:hover:[&>*]:underline tw-p-2\")).toBe(\"tw-p-2 [&>*]:underline inset-1 hover:inline text-black tabular-nums diagonal-fractions", 197 | "inset-y-1 p-3 [--scroll-offset:56px] dark:lg:hover:[&>*]:underline text-lg/8 dark:hover:[&:nth-child(3)]:py-0 dark:hover:[&:nth-child(3)]:py-0 whitespace-nowrap hyphens-auto p-2\")).toBe(\"p-2", 198 | "m-[10px]\")).toBe(\"m-[10px] -top-2000\")).toBe(\"-top-2000 inset-y-1 ring-2\")).toBe(\"shadow-md hidden\")).toBe(\"block brightness-90 [padding:1rem] inset-x-1 -inset-1 overflow-x-auto", 199 | "grayscale-0 from-0% inset-1\")).toBe(\"inset-1 bg-blue-500 brightness-90 focus:inline-1 p-2 tabular-nums hover_focus_!right-0 [&>*]:[&_div]:underline", 200 | "border-[color:rgb(var(--color-gray-500-rgb)/50%))] overflow-x-hidden inline-1\")).toBe(\"block text-red m-[10px]\")).toBe(\"m-[10px] my-[2px] focus:inline outline-dashed inset-x-1 w-full", 201 | "shadow inset-x-1\")).toBe(\"inset-x-1 hover:overflow-x-hidden border-white/10 focus__hover__!inset-0 peer-empty:p-3\")).toBe(\"peer-empty:p-3 ring bg-red-500 i-inline cursor-[grab]\")).toBe(\"cursor-[grab]", 202 | "hover:p-4 outline cursor-pointer from-red brightness-90 hover:inline\")).toBe(\"hover:inline p-2\")).toBe(\"p-2 line-clamp-2 justify-center p-2\")).toBe(\"p-2", 203 | "p-3 hover:bg-grey-5 p-2\")).toBe(\"p-3 bg-[color:var(--mystery-var)] w-fit\")).toBe(\"w-fit line-through block inline mix-blend-normal !right-2", 204 | "border-white/10\")).toBe(\"border-t line-clamp-[10] p-2\")).toBe(\"p-2 diagonal-fractions inline-1 dark:lg:hover:[&>*]:line-through inset-x-1\")).toBe(\"inset-x-1 text-black\")).toBe(\"text-3.5xl stroke-1 hover:block", 205 | "bg-red-500 bg-[color:var(--mystery-var)] shadow-md line-clamp-[10] p-4 bg-blue-500 !inset-0\")).toBe(\"!inset-0 hover:focus:p-2 bg-gradient-to-t outline-black", 206 | "shadow overflow-x-hidden !right-2 m-[2px] m-[2px] hidden\")).toBe(\"block ring\")).toBe(\"shadow hover:p-2 inset-x-px ring-2\")).toBe(\"shadow-md", 207 | "right-1 sticky right-1 normal-nums inset-y-1\")).toBe(\"inset-x-1 bg-hotpink\")).toBe(\"bg-hotpink hover:group-empty:p-3 [--scroll-offset:56px] shadow\")).toBe(\"ring border-white/10", 208 | "inset-x-1 inset-y-6 inset-1\")).toBe(\"inset-1 right-1 from-red\")).toBe(\"from-0% border-t-other-blue m-[10rem]\")).toBe(\"m-[10rem] [mask-type:luminance] [&:nth-child(3)]:py-0 ![some:another]", 209 | "group-empty:p-2 group-read-only:p-3 m-auto\")).toBe(\"m-auto p-3 focus:hover:m-[length:var(--c)] proportional-nums\")).toBe(\"proportional-nums my-[2px] grayscale-0 group-read-only:p-2 hyphens-auto", 210 | "dark:hover:[&:nth-child(3)]:py-0 bg-black bg-blue-500 right-4 outline no-underline\")).toBe(\"no-underline text-sm left-1\")).toBe(\"inset-1 inset-1\")).toBe(\"inset-1 group-read-only:p-3", 211 | "caption-top justify-stretch grid-rows-3 hover:focus:block focus:hover:!tw-inset-0 bg-gradient-to-t hover:group-empty:p-3 scale-[1.7]\")).toBe(\"scale-[1.7] focus:!inline group-read-only:p-2", 212 | "supports-[display:grid]:grid !tw-inset-0\")).toBe(\"!tw-inset-0 border-white stroke-2 [&>*]:[&_div]:underline outline whitespace-nowrap dark:lg:hover:[&>*]:line-through ring-2\")).toBe(\"shadow-md bg-none", 213 | "bg-origin-top [&_div]:line-through -top-2000\")).toBe(\"-top-2000 focus:!inline outline-black hover:block outline read-only:p-2 mix-blend-normal outline-dashed\")).toBe(\"outline-dashed", 214 | "px-2 justify-stretch hover:focus:!right-0 p-2 hover:block lining-nums m-[2px] supports-[display:grid]:flex focus:hover:!inset-0 hover:focus:m-[2px]", 215 | "inline peer-empty:p-3 normal-nums border-t [mask-type:luminance] outline-1 !font-medium no-underline\")).toBe(\"no-underline inset-y-1\")).toBe(\"inset-x-1 scale-[1.7]\")).toBe(\"scale-[1.7]", 216 | "content-['hello'] tabular-nums hover:block border-[color:rgb(var(--color-gray-500-rgb)/50%))] grow-[2]\")).toBe(\"grow-[2] line-clamp-[10] read-only:p-3\")).toBe(\"read-only:p-3 delay-0 border-white\")).toBe(\"border-t border-t-some-blue", 217 | "m-[10rem]\")).toBe(\"m-[10rem] caption-bottom\")).toBe(\"caption-bottom border-white/10\")).toBe(\"border-t brightness-90 line-clamp-2 p-2 border-white m-[10rem]\")).toBe(\"m-[10rem] focus:block m-[length:var(--mystery-var)]", 218 | "opacity-10 scale-[1.7]\")).toBe(\"scale-[1.7] [&:nth-child(3)]:py-4 z-[99]\")).toBe(\"z-[99] outline-1 w-full !-inset-x-1 hover_focus_!right-0 hidden\")).toBe(\"hidden outline-1", 219 | "-m-2 hover:block [some:other] block p-2 text-secret-sauce p-2 text-lg/7 -m-5\")).toBe(\"-m-5 p-5", 220 | "inset-x-1 overflow-x-hidden ![some:prop] shadow [&>*]:[&_div]:underline !font-medium p-2 inset-x-4 border-t shadow-md", 221 | "right-1 brightness-90 ring\")).toBe(\"shadow hover:bg-grey-5 text-secret-sauce shadow ring-2 p-3 block hidden\")).toBe(\"block", 222 | "overflow-x-auto hidden\")).toBe(\"block !font-bold line-clamp-none dark:lg:hover:[&>*]:line-through hidden\")).toBe(\"hidden hover:focus:!right-0 cursor-pointer [some:other] bg-grey-5", 223 | "border-[color:rgb(var(--color-gray-500-rgb)/50%))] grow inset-y-6 right-1 content-stretch p-5 hover:block content-['hello'] line-through line-through\")).toBe(\"line-through", 224 | "hover:left-1 overflow-x-hidden grid-cols-[1fr,auto] border-t-some-blue border-white/10\")).toBe(\"border-t ![some:prop] bg-black p-2\")).toBe(\"p-2 [mask-type:alpha] hover:block", 225 | "shadow-md hover:focus:block justify-center content-center hover:[paint-order:normal] overflow-x-scroll hover:m-[length:var(--c)] border-[color:rgb(var(--color-gray-500-rgb)/50%))] [&>*]:[&_div]:underline text-black\")).toBe(\"text-3.5xl", 226 | "content-[attr(data-content)] overflow-x-auto outline-dashed cursor-[grab]\")).toBe(\"cursor-[grab] hover:group-empty:p-3 bg-[color:var(--mystery-var)] inset-1\")).toBe(\"inset-1 z-[99]\")).toBe(\"z-[99] delay-150 text-red", 227 | "border overflow-x-hidden inset-1 left-1 tw-block inset-y-1 tw-p-2\")).toBe(\"tw-p-2 hover:empty:p-3\")).toBe(\"hover:empty:p-3 p-2 line-clamp-2", 228 | "tw-block -top-2000\")).toBe(\"-top-2000 [&>*]:[color:red] [&>*]:underline bg-grey-5 shadow-md grid-cols-2 underline from-0% hover:[paint-order:normal]", 229 | "outline-black hover:block\")).toBe(\"hover:block i-inline read-only:p-2 relative inline inset-x-1 outline-1 hover:block\")).toBe(\"hover:block outline", 230 | "px-5 shadow text-3.5xl inset-x-1\")).toBe(\"inset-x-1 bg-blue-500 inset-1\")).toBe(\"inset-1 shadow-md\")).toBe(\"ring-2 focus:hover:p-4 hover:focus:block line-through\")).toBe(\"line-through", 231 | "inset-y-1\")).toBe(\"inset-x-1 !tw-inset-0\")).toBe(\"!tw-inset-0 lining-nums diagonal-fractions static inset-x-px leading-6 inset-y-6 w-fit\")).toBe(\"w-fit dark:lg:hover:[&>*]:underline", 232 | "hover:block static inset-1 ![some:another] [&>*]:hover:line-through focus:inline text-red text-sm [&_div]:[&>*]:line-through grid-rows-3", 233 | "grid-rows-[1fr,auto] left-1 inset-1\")).toBe(\"inset-1 inline hover:m-[length:var(--c)] peer-empty:p-3\")).toBe(\"peer-empty:p-3 border-white/10\")).toBe(\"border-t justify-center group-read-only:p-3 ring", 234 | "focus:hover:p-4 p-5 tabular-nums hover:focus:!right-0 ![some:prop] !tw-right-0 dark:lg:hover:[&>*]:line-through grid-cols-2 border-white\")).toBe(\"border-t top-12", 235 | "hover:[paint-order:normal] group-empty:p-2 bg-[color:var(--mystery-var)] text-red inset-x-1 border-[color:rgb(var(--color-gray-500-rgb)/50%))] inset-x-1 z-20 bg-red-500 tw-p-3", 236 | "from-red justify-center [&>*]:[&_div]:line-through bg-none cursor-[grab]\")).toBe(\"cursor-[grab] text-black [&>*]:[&_div]:underline bg-red-500 block brightness-[1.75]\")).toBe(\"brightness-[1.75]", 237 | "inset-x-4 p-5 hidden\")).toBe(\"hidden grid-cols-2 border-white/10\")).toBe(\"border-t !-inset-x-px\")).toBe(\"!-inset-x-px hover:[paint-order:normal] right-1 bg-grey-5 peer-empty:p-2", 238 | "inset-1\")).toBe(\"inset-1 text-secret-sauce border-t-some-blue m-[2px] tw-p-2\")).toBe(\"tw-p-2 no-underline\")).toBe(\"no-underline group-empty:p-3\")).toBe(\"group-empty:p-3 !tw-right-0 inline-1\")).toBe(\"block font-thin", 239 | "[&:focus]:ring from-red delay-150 shadow\")).toBe(\"ring inline ![some:prop] inset-x-1 inset-x-1 grid-rows-[repeat(20,minmax(0,1fr))] outline-black", 240 | "!-inset-x-1 text-red inset-x-1 content-['hello'] stroke-2 p-5 m-[10px]\")).toBe(\"m-[10px] hover:[paint-order:normal] overflow-x-hidden [--scroll-offset:56px]", 241 | "line-through p-2 focus:hover:!inset-0 p-3 h-10 peer-empty:p-3\")).toBe(\"peer-empty:p-3 m-[10rem]\")).toBe(\"m-[10rem] focus__hover__!inset-0 bg-gradient-to-t focus:hover:m-[length:var(--c)]", 242 | "p-2 bg-gradient-to-t [some:other] p-2 w-fit\")).toBe(\"w-fit content-stretch -m-2 border-b [&>*]:hover:line-through p-4", 243 | "line-clamp-2 !right-2 peer-empty:p-3\")).toBe(\"peer-empty:p-3 text-secret-sauce [&[data-open]]:line-through [some:one] !font-medium shadow\")).toBe(\"ring h-10 hidden", 244 | "h-min\")).toBe(\"h-min hover:p-4 tabular-nums [&[data-open]]:line-through px-2 inset-x-1 hover:inline sticky [&>*]:line-through w-full", 245 | "from-red m-[10px]\")).toBe(\"m-[10px] scale-75 text-lg/8\")).toBe(\"text-red border-b tw-p-2\")).toBe(\"tw-p-2 focus:hover:!inset-0 focus:hover:inset-x-1 -m-2 focus:hover:!inset-0", 246 | "bg-red-500 inline inset-x-1\")).toBe(\"inset-x-1 stroke-[3]\")).toBe(\"stroke-[3] border-t-some-blue [paint-order:markers] hover:p-4 static underline brightness-90", 247 | "m-[2px] inset-x-1 supports-[display:grid]:grid -m-5\")).toBe(\"-m-5 [&:nth-child(3)]:py-4 inline justify-center m-[10rem]\")).toBe(\"m-[10rem] block inset-x-px", 248 | "peer-empty:p-3\")).toBe(\"peer-empty:p-3 inset-y-1 p-4 justify-normal dark:hover:[&:nth-child(3)]:py-0 supports-[display:grid]:grid !tw-right-0 hover:group-empty:p-3 caption-top hover:group-empty:p-3", 249 | "brightness-[1.75]\")).toBe(\"brightness-[1.75] hover:overflow-x-hidden focus:ring-4 p-2 p-3 inset-y-1 tabular-nums border-white\")).toBe(\"border-t no-underline\")).toBe(\"no-underline bg-gradient-to-t", 250 | "hover:m-[2px] hover:dark:[&:nth-child(3)]:py-4 inline text-sm bg-gradient-to-t hover:focus:m-[2px] p-2 inset-x-1 caption-top border-some-blue", 251 | "content-center ![some:another] peer-empty:p-2 [&:focus]:ring shadow sticky my-non-tailwind-class !right-2 outline-black outline-1", 252 | "text-lg/7 inline justify-stretch i-inline\")).toBe(\"block hover:focus:block from-red\")).toBe(\"from-0% !-inset-x-1 dark:hover:[&:nth-child(3)]:py-0 i-inline\")).toBe(\"block grow-[2]\")).toBe(\"grow-[2]", 253 | "empty:p-2 inset-x-1 normal-nums ring [&[data-open]]:line-through grid-rows-2\")).toBe(\"grid-rows-2 grid-rows-3 border-white/10\")).toBe(\"border-t overflow-x-auto [padding:1rem]", 254 | "hover:inline inset-1\")).toBe(\"inset-1 outline-black text-red !font-bold\")).toBe(\"!font-bold inset-x-1 [-unknown-prop:::123:::] text-red block\")).toBe(\"block [some:other]", 255 | "border-t shadow h-10 m-[10rem]\")).toBe(\"m-[10rem] supports-[display:grid]:grid supports-[display:grid]:grid right-1 tabular-nums [paint-order:normal] block", 256 | "from-0% opacity-[0.025]\")).toBe(\"opacity-[0.025] hover:focus:m-[2px] inset-x-1 right-4 bg-none !right-0 block\")).toBe(\"block !tw-inset-0\")).toBe(\"!tw-inset-0 hover:inline\")).toBe(\"hover:inline", 257 | "[&>*]:line-through focus:hover:!tw-inset-0 focus:inline !right-0 left-1\")).toBe(\"inset-1 block\")).toBe(\"block [paint-order:markers] -top-12 [mask-type:alpha] read-only:p-3\")).toBe(\"read-only:p-3", 258 | "ring-2\")).toBe(\"shadow-md inset-x-1\")).toBe(\"inset-x-1 overflow-x-auto p-3 ring outline-1 bg-black brightness-90 whitespace-break-spaces inset-1", 259 | "p-2 border-t no-underline\")).toBe(\"no-underline tw-p-3 inline -top-12 hover:group-empty:p-2 inset-x-1\")).toBe(\"inset-x-1 outline-dashed hover:empty:p-2", 260 | "py-1 right-4 overflow-x-auto block brightness-[1.75]\")).toBe(\"brightness-[1.75] [&_div]:[&>*]:line-through underline lining-nums [&>*]:[color:blue] px-5", 261 | "shadow-md group-empty:p-2 outline text-secret-sauce bg-gradient-to-t p-4 scale-75 my-non-tailwind-class [&_div]:line-through ![some:another]", 262 | "!-inset-x-1 content-[attr(data-content)] whitespace-nowrap inset-x-4 mix-blend-multiply block block duration-150 !-inset-x-1 bg-hotpink\")).toBe(\"bg-hotpink", 263 | "grid-cols-2 text-lg/7 bottom-auto from-0% border-b focus:hover:!inset-0 !p-3 peer-empty:p-2 text-red inline", 264 | "supports-[display:grid]:flex i-inline\")).toBe(\"block -inset-1 hover:inline justify-center bottom-auto !font-bold hover:focus:!tw-right-0 hover:focus:p-2 [&[data-open]]:underline", 265 | "[&_div]:[&>*]:line-through ring\")).toBe(\"shadow bg-origin-top bg-origin-top tw-p-3 focus:inline hover:m-[length:var(--c)] ![some:prop] border-white/10\")).toBe(\"border-t outline-dashed", 266 | "hover:focus:m-[2px] focus:inline p-3 [&>*]:underline no-underline\")).toBe(\"no-underline ![some:another] inset-y-1 inset-x-1 text-lg/8 bottom-auto", 267 | "group-empty:p-3\")).toBe(\"group-empty:p-3 [some:one] no-underline\")).toBe(\"no-underline border-[color:rgb(var(--color-gray-500-rgb)/50%))] grid-cols-[1fr,auto] bg-[color:var(--mystery-var)] inset-x-1 peer-empty:p-2 diagonal-fractions hover:[&>*]:underline", 268 | "inset-y-1 focus:!block\")).toBe(\"focus:!block content-[attr(data-content)] right-1 inset-x-1 inset-x-1 !right-2 inset-x-1 leading-6 stroke-2", 269 | "inline font-thin outline-dashed inset-1 bg-blue-500 text-black top-12 [padding:1rem] hover:block hover:focus:!right-0", 270 | "font-thin opacity-[0.025]\")).toBe(\"opacity-[0.025] hover:focus:block hyphens-auto border-white\")).toBe(\"border-t i-inline\")).toBe(\"block hyphens-manual\")).toBe(\"hyphens-manual text-lg/7 group-empty:p-2 [&>*]:[&_div]:underline", 271 | "block inset-1\")).toBe(\"inset-1 caption-top opacity-[0.025]\")).toBe(\"opacity-[0.025] dark:lg:hover:[&>*]:line-through focus:inline border-t-some-blue hover:inline\")).toBe(\"hover:inline p-4 diagonal-fractions", 272 | "inset-y-6 grid-rows-[repeat(20,minmax(0,1fr))] -m-2 peer-empty:p-2 hover:m-[length:var(--c)] border-b inset-y-1 z-20 opacity-10 px-5", 273 | "scale-75 -m-2 [paint-order:markers] ![some:another] -top-69\")).toBe(\"-top-69 focus__hover__!inset-0 -inset-1 hyphens-auto from-red ring", 274 | "empty:p-3\")).toBe(\"empty:p-3 [&_div]:[&>*]:line-through !tw-inset-0\")).toBe(\"!tw-inset-0 read-only:p-3\")).toBe(\"read-only:p-3 i-inline -top-2000\")).toBe(\"-top-2000 outline-1 [&>*]:hover:line-through stroke-1\")).toBe(\"stroke-black [&>*]:[&_div]:line-through", 275 | "overflow-x-hidden [&>*]:[&_div]:line-through hidden block\")).toBe(\"block !inset-0\")).toBe(\"!inset-0 !right-0 p-5 hover:empty:p-3\")).toBe(\"hover:empty:p-3 inset-x-1\")).toBe(\"inset-1 tw-block", 276 | "stroke-2 no-underline\")).toBe(\"no-underline cursor-[grab]\")).toBe(\"cursor-[grab] -right-1 grow-[2]\")).toBe(\"grow-[2] hover:[&>*]:underline bg-gradient-to-t grid-rows-[repeat(20,minmax(0,1fr))] overflow-x-hidden block", 277 | "![some:prop] bg-origin-top hover:empty:p-3\")).toBe(\"hover:empty:p-3 inset-y-1\")).toBe(\"inset-x-1 [&>*]:[&_div]:line-through text-red [&>*]:underline grid-rows-[1fr,auto] hover:focus:block outline", 278 | "p-3 p-3 ring !font-medium text-lg/7 opacity-10 underline block [paint-order:markers] hover_focus_!right-0", 279 | "inset-y-1 inline border-white\")).toBe(\"border-t static !right-2 non-tailwind-class border-t overflow-x-hidden dark:lg:hover:[&>*]:line-through border-[color:rgb(var(--color-gray-500-rgb)/50%))]", 280 | "stroke-[3]\")).toBe(\"stroke-[3] ![some:prop] inset-x-1 bg-none bg-black proportional-nums\")).toBe(\"proportional-nums m-[length:var(--mystery-var)] non-tailwind-class -inset-1 diagonal-fractions", 281 | "underline -inset-1 focus:inline-1 dark:lg:hover:[&>*]:line-through dark:lg:hover:[&>*]:underline hover:block focus:!inline [&>*]:[&_div]:underline block hover:p-2", 282 | "outline-black lining-nums border-t-other-blue dark:hover:lg:[&>*]:line-through cursor-[grab]\")).toBe(\"cursor-[grab] [paint-order:normal] hover:bg-grey-5 [&:nth-child(3)]:py-0 p-3 bottom-auto", 283 | "m-[length:var(--mystery-var)] z-20 inset-x-1 leading-6 -right-1 p-5 inset-y-1 shadow ring focus:hover:inset-x-1", 284 | "hover:bg-hotpink\")).toBe(\"hover:bg-hotpink line-through\")).toBe(\"line-through !right-2 i-inline rounded !font-bold outline-black !font-bold\")).toBe(\"!font-bold text-black stroke-1\")).toBe(\"stroke-black", 285 | "left-1 -top-69\")).toBe(\"-top-69 text-red underline inset-x-1 right-1 caption-top p-5 hover:focus:m-[2px] inline-1", 286 | "inset-x-1 inline inline non-tailwind-class [&>*]:hover:line-through border-white content-['hello'] supports-[display:grid]:grid grayscale-0 p-2", 287 | "peer-empty:p-2 grid-cols-2 [mask-type:luminance] p-4 inset-y-6 inset-y-1 -m-2 !tw-right-0 outline-dashed\")).toBe(\"outline-dashed m-[2px]", 288 | "scale-[1.7]\")).toBe(\"scale-[1.7] focus:hover:!inset-0 border bg-none block\")).toBe(\"block dark:lg:hover:[&>*]:line-through [mask-type:luminance] px-2 justify-stretch tw-block", 289 | "right-1 [paint-order:normal] right-1 hover:focus:!right-0 group-empty:p-2 p-5 mix-blend-normal overflow-x-auto hover:empty:p-3\")).toBe(\"hover:empty:p-3 inset-x-1", 290 | "shadow\")).toBe(\"ring ring hover:group-empty:p-3 grid-rows-3 my-[2px] bg-origin-top -top-2000\")).toBe(\"-top-2000 stroke-2 whitespace-nowrap dark:lg:hover:[&>*]:line-through", 291 | "!inset-0\")).toBe(\"!inset-0 duration-150 hover:m-[2px] focus:hover:m-[length:var(--c)] scale-[1.7]\")).toBe(\"scale-[1.7] tabular-nums opacity-[0.025]\")).toBe(\"opacity-[0.025] peer-empty:p-2 my-[2px] border-b", 292 | "text-black\")).toBe(\"text-3.5xl hover:p-4 block inset-x-4 m-[2px] [&>*]:[color:red] group-read-only:p-2 !tw-inset-0\")).toBe(\"!tw-inset-0 non-tailwind-class tabular-nums", 293 | "border-some-blue brightness-[1.75]\")).toBe(\"brightness-[1.75] block inset-y-6 inline ring border-white/10 rounded p-2\")).toBe(\"p-3 brightness-[1.75]\")).toBe(\"brightness-[1.75]", 294 | "!right-0 line-clamp-none z-20 hidden\")).toBe(\"hidden p-4 grow-[2]\")).toBe(\"grow-[2] m-[10rem]\")).toBe(\"m-[10rem] hover:[paint-order:normal] grow-[2]\")).toBe(\"grow-[2] [&>*]:line-through", 295 | "hover:inline\")).toBe(\"hover:inline bg-black hover:[paint-order:markers] hover:block shadow caption-top overflow-x-auto lining-nums inset-y-1\")).toBe(\"inset-x-1 grid-rows-2\")).toBe(\"grid-rows-2", 296 | "border-white\")).toBe(\"border-t [&>*]:[&_div]:underline bg-hotpink\")).toBe(\"bg-hotpink -m-2 no-underline\")).toBe(\"no-underline inset-x-1\")).toBe(\"inset-1 justify-stretch -m-2 tw-hidden\")).toBe(\"tw-hidden block", 297 | "left-1 outline-black [&>*]:[&_div]:underline inline border-white/10\")).toBe(\"border-t -m-2 outline-black content-[attr(data-content)] p-3 read-only:p-2", 298 | "text-3.5xl inset-1\")).toBe(\"inset-1 delay-150 [&>*]:underline [&[data-open]]:line-through p-2 inset-x-px overflow-x-auto right-1 text-secret-sauce", 299 | "bg-black border-white/10\")).toBe(\"border-t p-5 p-5 text-black\")).toBe(\"text-3.5xl p-2 border-white supports-[display:grid]:flex bg-[color:var(--mystery-var)] cursor-pointer", 300 | "diagonal-fractions p-3 line-through\")).toBe(\"line-through text-red duration-150 cursor-pointer cursor-pointer i-inline\")).toBe(\"block bg-red-500 text-lg/7", 301 | "focus:block [paint-order:markers] focus:ring-4 text-lg/8\")).toBe(\"text-red hover:m-[length:var(--c)] m-[2px] i-inline outline [&>*]:line-through hover:focus:!right-0", 302 | "border focus:hover:inset-x-1 empty:p-3\")).toBe(\"empty:p-3 !right-2 hover:group-empty:p-2 group-read-only:p-3 !font-bold block outline-black tw-p-3", 303 | "inset-x-px [-unknown-prop:url(https://hi.com)] bg-hotpink\")).toBe(\"bg-hotpink hover:p-4 text-sm [&>*]:line-through cursor-[grab]\")).toBe(\"cursor-[grab] hover:[&>*]:underline [paint-order:normal] [&>*]:underline", 304 | "shadow peer-empty:p-3\")).toBe(\"peer-empty:p-3 hover:bg-grey-5 hover__focus__!right-0 cursor-[grab]\")).toBe(\"cursor-[grab] overflow-x-scroll !font-bold hover:empty:p-2 right-4 focus:hover:!inset-0", 305 | "border from-red\")).toBe(\"from-0% group-read-only:p-3 inset-y-1 px-2 px-2 content-[attr(data-content)] outline grid-rows-2\")).toBe(\"grid-rows-2 z-[99]\")).toBe(\"z-[99]", 306 | "[mask-type:luminance] p-3 h-min\")).toBe(\"h-min from-red border-b shadow\")).toBe(\"ring inset-x-1 !font-bold\")).toBe(\"!font-bold line-clamp-[10] overflow-x-scroll", 307 | "content-stretch p-2\")).toBe(\"p-3 normal-nums [some:other] tw-p-3 block caption-top !-inset-x-px\")).toBe(\"!-inset-x-px inset-x-1 delay-0", 308 | "dark:lg:hover:[&>*]:line-through hover:focus:-right-1 diagonal-fractions p-3 hover:bg-hotpink\")).toBe(\"hover:bg-hotpink [-unknown-prop:::123:::] line-clamp-[10] !font-medium px-2 hover:bg-hotpink\")).toBe(\"hover:bg-hotpink", 309 | "p-4 i-inline\")).toBe(\"block hover:left-1 brightness-90 block duration-150 lg:[--scroll-offset:44px] normal-nums p-5 grid-rows-[1fr,auto]", 310 | "!-inset-x-px\")).toBe(\"!-inset-x-px focus:hover:p-4 inset-y-1\")).toBe(\"inset-x-1 hover:group-empty:p-3 m-[calc(100%-var(--arbitrary))] p-2 inset-x-1 m-[2px] focus:hover:!tw-inset-0 focus:ring-4", 311 | "shadow-md\")).toBe(\"ring-2 dark:hover:[&:nth-child(3)]:py-0 border-t-some-blue border group-empty:p-3\")).toBe(\"group-empty:p-3 overflow-x-auto !font-medium my-[2px] inset-y-1\")).toBe(\"inset-x-1 text-lg/8\")).toBe(\"text-red", 312 | "my-non-tailwind-class inline outline text-3.5xl inset-1 [&>*]:[color:red] [&>*]:underline p-4 inline group-empty:p-2", 313 | "-top-69\")).toBe(\"-top-69 stroke-1 hover_focus_!right-0 p-2 block focus:!inline -m-2 focus:hover:inset-x-1 -m-2 -top-12", 314 | "border-[color:rgb(var(--color-gray-500-rgb)/50%))] border-white -m-2 border-t-some-blue [&>*]:underline p-2\")).toBe(\"p-3 supports-[display:grid]:flex [&>*]:hover:line-through lining-nums normal-nums", 315 | "from-red !right-0 hover__focus__!right-0 hover:dark:[&:nth-child(3)]:py-4 inset-1 p-2\")).toBe(\"p-3 !font-medium grid-cols-2 stroke-1\")).toBe(\"stroke-black ring-2\")).toBe(\"shadow-md", 316 | "text-red hidden\")).toBe(\"hidden block bg-gradient-to-t diagonal-fractions [-unknown-prop:url(https://hi.com)] ring-2 text-lg/8\")).toBe(\"text-red [&:nth-child(3)]:py-0 text-secret-sauce", 317 | "lg:[--scroll-offset:44px] hover:inline tw-p-2\")).toBe(\"tw-p-2 [&>*]:line-through block w-fit\")).toBe(\"w-fit bg-blue-500 group-read-only:p-3 inset-x-1 inset-x-1", 318 | "[&>*]:[&_div]:underline ring-2 shadow\")).toBe(\"ring inline right-1 inset-x-1 [&:focus]:ring border-[color:rgb(var(--color-gray-500-rgb)/50%))] text-lg/8\")).toBe(\"text-red hover:[paint-order:markers]", 319 | "z-20 bg-blue-500 !font-medium outline bg-blue-500 [padding:1rem] outline inset-y-6 tw-hidden\")).toBe(\"tw-hidden text-lg/7", 320 | "border-t h-10 grid-rows-3 hover:p-4 hover:left-1 bg-blue-500 caption-top h-10 from-0% duration-150", 321 | "my-[2px] proportional-nums\")).toBe(\"proportional-nums -top-2000\")).toBe(\"-top-2000 bg-origin-top p-2\")).toBe(\"p-3 hidden\")).toBe(\"hidden border-white tabular-nums static p-2\")).toBe(\"p-2", 322 | "top-12 block block\")).toBe(\"block outline-dashed\")).toBe(\"outline-dashed focus:block cursor-pointer m-[length:var(--mystery-var)] hover:p-4 left-1 text-lg/7", 323 | "grid-rows-[repeat(20,minmax(0,1fr))] !-inset-x-1 shadow !font-medium inline hover:block hover:block focus:hover:inset-x-1 bg-gradient-to-t hyphens-manual\")).toBe(\"hyphens-manual", 324 | "inset-1 border-[color:rgb(var(--color-gray-500-rgb)/50%))] hover:empty:p-2 !tw-inset-0\")).toBe(\"!tw-inset-0 content-[attr(data-content)] grayscale-0 !right-2 delay-150 from-0% block", 325 | "my-non-tailwind-class hover:focus:!right-0 no-underline\")).toBe(\"no-underline [some:other] diagonal-fractions -top-12 hyphens-auto text-red tabular-nums peer-empty:p-3\")).toBe(\"peer-empty:p-3", 326 | "p-2 overflow-x-hidden !-inset-x-1 inset-x-px p-5 outline-black hover:[paint-order:markers] stroke-2 border-t-some-blue stroke-black", 327 | "grid-cols-2 proportional-nums\")).toBe(\"proportional-nums !tw-right-0 content-['hello'] hover:focus:!right-0 diagonal-fractions border-t justify-normal right-1 text-lg/7", 328 | "bg-hotpink\")).toBe(\"bg-hotpink border-t-some-blue dark:lg:hover:[&>*]:underline ring-2 dark:lg:hover:[&>*]:underline focus:hover:m-[length:var(--c)] [&>*]:line-through inset-1\")).toBe(\"inset-1 mix-blend-multiply tw-p-2\")).toBe(\"tw-p-2", 329 | "brightness-[1.75]\")).toBe(\"brightness-[1.75] stroke-2 overflow-x-hidden h-10 opacity-[0.025]\")).toBe(\"opacity-[0.025] [--scroll-offset:56px] hover:block hover:p-4 mix-blend-normal inset-x-1", 330 | "inset-x-1 [&>*]:hover:line-through m-[10rem]\")).toBe(\"m-[10rem] line-clamp-[10] [some:other] !p-4 overflow-x-scroll stroke-black inset-x-1 p-2\")).toBe(\"p-3", 331 | "stroke-2 ![some:prop] h-10 !right-2 opacity-10 !right-0 inset-1\")).toBe(\"inset-1 border-t-other-blue overflow-x-auto supports-[display:grid]:flex", 332 | "p-3 hover:dark:[&:nth-child(3)]:py-4 bg-none outline !inset-0\")).toBe(\"!inset-0 !tw-right-0 !tw-right-0 focus:hover:m-[length:var(--c)] hover:dark:[&:nth-child(3)]:py-4 dark:lg:hover:[&>*]:underline", 333 | "mix-blend-normal focus:hover:!inset-0 border-b grid-cols-2 bg-blue-500 [&>*]:[&_div]:underline bg-red-500 brightness-[1.75]\")).toBe(\"brightness-[1.75] hover:empty:p-3\")).toBe(\"hover:empty:p-3 !tw-right-0", 334 | "border-t -top-12 line-clamp-2 m-[length:var(--mystery-var)] hover:[paint-order:normal] hover:p-2 text-black stroke-1 hover:empty:p-2 inset-x-1", 335 | "!inset-0\")).toBe(\"!inset-0 hover:block hover:empty:p-2 mix-blend-normal ![some:prop] border-t my-[2px] inset-x-1 shadow-md\")).toBe(\"ring-2 inset-x-1\")).toBe(\"inset-1", 336 | "justify-stretch border-b [paint-order:normal] !right-0 grid-rows-[1fr,auto] !tw-inset-0\")).toBe(\"!tw-inset-0 justify-normal right-1 opacity-[0.025]\")).toBe(\"opacity-[0.025] p-3", 337 | "group-read-only:p-2 [&>*]:underline border-t-some-blue inset-y-1 focus:block my-[2px] from-red\")).toBe(\"from-0% px-5 shadow-md hidden\")).toBe(\"hidden", 338 | "inset-1 inline content-[attr(data-content)] border-b inset-x-1 bottom-auto focus:hover:!inset-0 hover:dark:[&:nth-child(3)]:py-4 non-tailwind-class px-5", 339 | "inline inline-1 hover:[&>*]:underline focus:!inline hover:focus:!tw-right-0 [&:nth-child(3)]:py-4 cursor-[grab]\")).toBe(\"cursor-[grab] inset-x-1 text-red stroke-[3]\")).toBe(\"stroke-[3]", 340 | "relative stroke-[3]\")).toBe(\"stroke-[3] brightness-90 line-through p-5 peer-empty:p-2 px-2 border-t ring-2 text-lg/7", 341 | "focus:hover:!tw-inset-0 justify-stretch group-read-only:p-3 text-lg/8\")).toBe(\"text-red right-1 hover:m-[length:var(--c)] grayscale-[50%]\")).toBe(\"grayscale-[50%] [&>*]:[&_div]:line-through grayscale-0 p-2", 342 | "p-3 [&>*]:[&_div]:line-through !right-0 line-clamp-[10] underline bg-hotpink\")).toBe(\"bg-hotpink focus__hover__!inset-0 !inset-0\")).toBe(\"!inset-0 focus:hover:!inset-0 delay-150", 343 | "hover:[paint-order:normal] hover:p-4 block my-non-tailwind-class shadow z-20 grayscale-0 border-[color:rgb(var(--color-gray-500-rgb)/50%))] hover:p-4 duration-150", 344 | "grid-rows-3 tabular-nums !-inset-x-px\")).toBe(\"!-inset-x-px px-2 !-inset-x-1 inline peer-empty:p-3 hidden\")).toBe(\"hidden rounded tw-block", 345 | "[-unknown-prop:url(https://hi.com)] shadow-md hover:[paint-order:normal] dark:lg:hover:[&>*]:underline shadow hover:[&>*]:underline scale-[1.7]\")).toBe(\"scale-[1.7] brightness-90 -inset-1 grid-cols-[1fr,auto]", 346 | "m-[calc(100%-var(--arbitrary))] inset-x-px shadow-md hover:focus:!right-0 justify-stretch [paint-order:markers] text-red [&>*]:underline border-t-some-blue w-full", 347 | "text-lg/8\")).toBe(\"text-red ring-2\")).toBe(\"shadow-md hover:focus:!right-0 proportional-nums\")).toBe(\"proportional-nums inset-x-4 caption-bottom\")).toBe(\"caption-bottom outline-dashed left-1 duration-0 bg-gradient-to-t", 348 | "inset-1\")).toBe(\"inset-1 content-['hello'] inset-1\")).toBe(\"inset-1 focus:ring-4 hover:bg-grey-5 right-1 border-some-blue inset-x-1 line-through z-20", 349 | "shadow\")).toBe(\"ring outline whitespace-break-spaces shadow tw-hidden\")).toBe(\"tw-hidden caption-bottom\")).toBe(\"caption-bottom inset-x-1 outline read-only:p-3\")).toBe(\"read-only:p-3 group-read-only:p-3" 350 | ] -------------------------------------------------------------------------------- /benchmark/shared.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import inputs from "./_generated/inputs.json"; 3 | 4 | function bench(fn: () => void) { 5 | const start = Date.now(); 6 | fn(); 7 | const end = Date.now(); 8 | return end - start; 9 | } 10 | 11 | function run(merge: (input: string) => string, times: number) { 12 | while (times > 0) { 13 | inputs.forEach((input) => merge(input)); 14 | times--; 15 | } 16 | } 17 | 18 | function wait(ms = 1000) { 19 | return new Promise((resolve) => setTimeout(resolve, ms)); 20 | } 21 | 22 | export async function benchmark( 23 | createMerge: () => (input: string) => string, 24 | { 25 | warmUpTimes = 100000, 26 | times = 1000000, 27 | }: { times?: number; warmUpTimes?: number } = {} 28 | ) { 29 | // create merge 30 | let merge: ReturnType; 31 | const creationTime = bench(() => { 32 | merge = createMerge(); 33 | }); 34 | console.log(`\nCreation time: ${creationTime} ms`); 35 | 36 | // first call time 37 | const firstCallTime = bench(() => { 38 | merge(""); 39 | }); 40 | console.log(`\nFirst call time: ${firstCallTime} ms`); 41 | 42 | // warm up 43 | console.log("\nWarming up..."); 44 | await wait(); 45 | run(merge!, warmUpTimes); 46 | await wait(); 47 | 48 | // run 49 | console.log( 50 | `\nThe merge function will be tested ${times} times with ${ 51 | inputs.length 52 | } inputs for a total of ${times * inputs.length} function calls.\n` 53 | ); 54 | console.log("Running benchmark..."); 55 | const time = bench(() => run(merge, times)); 56 | console.log(`\nTotal time: ${time} ms`); 57 | } 58 | -------------------------------------------------------------------------------- /benchmark/tailwind-merge-cacheless.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { extendTailwindMerge } from "tailwind-merge"; 3 | 4 | import { benchmark } from "./shared"; 5 | 6 | console.log("\n\nBenchmarking tailwind-merge (no cache)...\n"); 7 | 8 | const create = () => extendTailwindMerge({ cacheSize: 0 }); 9 | benchmark(create, { warmUpTimes: 100, times: 1000 }); 10 | -------------------------------------------------------------------------------- /benchmark/tailwind-merge.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { extendTailwindMerge } from "tailwind-merge"; 3 | 4 | import { benchmark } from "./shared"; 5 | 6 | async function main() { 7 | const create = () => extendTailwindMerge({ cacheSize: 500 }); 8 | 9 | console.log("\n\nBenchmarking tailwind-merge (no cycles on warmup)...\n"); 10 | await benchmark(create, { warmUpTimes: 0 }); 11 | 12 | console.log("\n\nBenchmarking tailwind-merge...\n"); 13 | benchmark(create); 14 | } 15 | 16 | main(); 17 | -------------------------------------------------------------------------------- /benchmark/tw-merge-cacheless.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { createMerge, tailwind } from "../src/index"; 3 | 4 | import { benchmark } from "./shared"; 5 | 6 | console.log("\n\nBenchmarking tw-merge (no cache)...\n"); 7 | 8 | const create = () => createMerge(tailwind(), { cacheSize: 0 }); 9 | benchmark(create, { warmUpTimes: 100, times: 1000 }); 10 | -------------------------------------------------------------------------------- /benchmark/tw-merge.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { createMerge, tailwind } from "../src/index"; 3 | 4 | import { benchmark } from "./shared"; 5 | 6 | async function main() { 7 | const create = () => createMerge(tailwind(), { cacheSize: 500 }); 8 | 9 | console.log("\n\nBenchmarking tw-merge (no cycles on warmup)...\n"); 10 | await benchmark(create, { warmUpTimes: 0 }); 11 | 12 | console.log("\n\nBenchmarking tw-merge...\n"); 13 | benchmark(create); 14 | } 15 | 16 | main(); 17 | -------------------------------------------------------------------------------- /docs/api-reference.md: -------------------------------------------------------------------------------- 1 | # API reference 2 | 3 | Reference to all exports of tailwind-merge. 4 | 5 | ## `twMerge` 6 | 7 | ```ts 8 | function twMerge( 9 | ...classLists: Array< 10 | string | undefined | null | false | 0 | typeof classLists 11 | > 12 | ): string; 13 | ``` 14 | 15 | Default function to use if you're using the default Tailwind config or are close enough to the default config. Check out [basic usage](./configuration.md#basic-usage) for more info. 16 | 17 | If `twMerge` doesn't work for you, you can create your own custom merge function with [`extendTailwindMerge`](#extendtailwindmerge). 18 | 19 | ## `twJoin` 20 | 21 | ```ts 22 | function twJoin( 23 | ...classLists: Array< 24 | string | undefined | null | false | 0 | typeof classLists 25 | > 26 | ): string; 27 | ``` 28 | 29 | Function to join className strings conditionally without resolving conflicts. 30 | 31 | ```ts 32 | twJoin( 33 | "border border-red-500", 34 | hasBackground && "bg-red-100", 35 | hasLargeText && "text-lg", 36 | hasLargeSpacing && ["p-2", hasLargeText ? "leading-8" : "leading-7"] 37 | ); 38 | ``` 39 | 40 | It is used internally within `twMerge` and a direct subset of [`clsx`](https://www.npmjs.com/package/clsx). If you use `clsx` or [`classnames`](https://www.npmjs.com/package/classnames) to apply Tailwind classes conditionally and don't need support for object arguments, you can use `twJoin` instead, it is a little faster and will save you a few hundred bytes in bundle size. 41 | 42 | ## `getDefaultConfig` 43 | 44 | ```ts 45 | function getDefaultConfig(): Config; 46 | ``` 47 | 48 | Function which returns the default config used by tailwind-merge. The tailwind-merge config is different from the Tailwind config. It is optimized for small bundle size and fast runtime performance because it is expected to run in the browser. 49 | 50 | ## `fromTheme` 51 | 52 | ```ts 53 | function fromTheme(key: string): ThemeGetter; 54 | ``` 55 | 56 | Function to retrieve values from a theme scale, to be used in class groups. 57 | 58 | `fromTheme` doesn't return the values from the theme scale, but rather another function which is used by tailwind-merge internally to retrieve the theme values. tailwind-merge can differentiate the theme getter function from a validator because it has a `isThemeGetter` property set to `true`. 59 | 60 | It can be used like this: 61 | 62 | ```ts 63 | extendTailwindMerge({ 64 | theme: { 65 | 'my-scale': ['foo', 'bar'] 66 | }, 67 | classGroups: { 68 | 'my-group': [{ 'my-group': [fromTheme('my-scale'), fromTheme('spacing')] }] 69 | 'my-group-x': [{ 'my-group-x': [fromTheme('my-scale')] }] 70 | } 71 | }) 72 | ``` 73 | 74 | ## `extendTailwindMerge` 75 | 76 | ```ts 77 | function extendTailwindMerge( 78 | configExtension: Partial, 79 | ...createConfig: Array<(config: Config) => Config> 80 | ): TailwindMerge; 81 | function extendTailwindMerge( 82 | ...createConfig: Array<(config: Config) => Config> 83 | ): TailwindMerge; 84 | ``` 85 | 86 | Function to create merge function with custom config which extends the default config. Use this if you use the default Tailwind config and just extend it in some places. 87 | 88 | You provide it a `configExtension` object which gets [merged](#mergeconfigs) with the default config. 89 | 90 | ```ts 91 | const customTwMerge = extendTailwindMerge({ 92 | cacheSize: 0, // ← Disabling cache 93 | // ↓ Optional prefix from TaiLwind config 94 | prefix: "tw-", 95 | // ↓ Optional separator from TaiLwind config 96 | separator: "_", 97 | // ↓ Add values to existing theme scale or create a new one 98 | // Not all theme keys form the Tailwind config are supported by default. 99 | theme: { 100 | spacing: ["sm", "md", "lg"], 101 | }, 102 | // ↓ Here you define class groups 103 | classGroups: { 104 | // ↓ The `foo` key here is the class group ID 105 | // ↓ Creates group of classes which have conflicting styles 106 | // Classes here: foo, foo-2, bar-baz, bar-baz-1, bar-baz-2 107 | foo: ["foo", "foo-2", { "bar-baz": ["", "1", "2"] }], 108 | // ↓ Functions can also be used to match classes. 109 | // Classes here: qux-auto, qux-1000, qux-1001,… 110 | bar: [{ qux: ["auto", (value) => Number(value) >= 1000] }], 111 | baz: ["baz-sm", "baz-md", "baz-lg"], 112 | }, 113 | // ↓ Here you can define additional conflicts across different groups 114 | conflictingClassGroups: { 115 | // ↓ ID of class group which creates a conflict with… 116 | // ↓ …classes from groups with these IDs 117 | // In this case `twMerge('qux-auto foo') → 'foo'` 118 | foo: ["bar"], 119 | }, 120 | // ↓ Here you can define conflicts between the postfix modifier of a group and a different class group. 121 | conflictingClassGroupModifiers: { 122 | // ↓ ID of class group whose postfix modifier creates a conflict with… 123 | // ↓ …classes from groups with these IDs 124 | // In this case `twMerge('qux-auto baz-sm/1000') → 'baz-sm/1000'` 125 | baz: ["bar"], 126 | }, 127 | }); 128 | ``` 129 | 130 | Additionally, you can pass multiple `createConfig` functions (more to that in [`createTailwindMerge`](#createtailwindmerge)) which is convenient if you want to combine your config with third-party plugins. 131 | 132 | ```ts 133 | const customTwMerge = extendTailwindMerge({ … }, withSomePlugin) 134 | ``` 135 | 136 | If you only use plugins, you can omit the `configExtension` object as well. 137 | 138 | ```ts 139 | const customTwMerge = extendTailwindMerge(withSomePlugin); 140 | ``` 141 | 142 | ## `createTailwindMerge` 143 | 144 | ```ts 145 | function createTailwindMerge( 146 | ...createConfig: [() => Config, ...Array<(config: Config) => Config>] 147 | ): TailwindMerge; 148 | ``` 149 | 150 | Function to create merge function with custom config. Use this function instead of [`extendTailwindMerge`](#extendtailwindmerge) if you don't need the default config or want more control over the config. 151 | 152 | You need to provide a function which resolves to the config tailwind-merge should use for the new merge function. You can either extend from the default config or create a new one from scratch. 153 | 154 | ```ts 155 | // ↓ Callback passed to `createTailwindMerge` is called when 156 | // `customTwMerge` gets called the first time. 157 | const customTwMerge = createTailwindMerge(() => { 158 | const defaultConfig = getDefaultConfig(); 159 | 160 | return { 161 | cacheSize: 0, 162 | classGroups: { 163 | ...defaultConfig.classGroups, 164 | foo: ["foo", "foo-2", { "bar-baz": ["", "1", "2"] }], 165 | bar: [{ qux: ["auto", (value) => Number(value) >= 1000] }], 166 | baz: ["baz-sm", "baz-md", "baz-lg"], 167 | }, 168 | conflictingClassGroups: { 169 | ...defaultConfig.conflictingClassGroups, 170 | foo: ["bar"], 171 | }, 172 | conflictingClassGroupModifiers: { 173 | ...defaultConfig.conflictingClassGroupModifiers, 174 | baz: ["bar"], 175 | }, 176 | }; 177 | }); 178 | ``` 179 | 180 | Same as in [`extendTailwindMerge`](#extendtailwindmerge) you can use multiple `createConfig` functions which is convenient if you want to combine your config with third-party plugins. Just keep in mind that the first `createConfig` function does not get passed any arguments, whereas the subsequent functions get each passed the config from the previous function. 181 | 182 | ```ts 183 | const customTwMerge = createTailwindMerge( 184 | getDefaultConfig, 185 | withSomePlugin, 186 | (config) => ({ 187 | // ↓ Config returned by `withSomePlugin` 188 | ...config, 189 | classGroups: { 190 | ...config.classGroups, 191 | mySpecialClassGroup: [{ special: ["1", "2"] }], 192 | }, 193 | }) 194 | ); 195 | ``` 196 | 197 | But don't merge configs like that. Use [`mergeConfigs`](#mergeconfigs) instead. 198 | 199 | ## `mergeConfigs` 200 | 201 | ```ts 202 | function mergeConfigs( 203 | baseConfig: Config, 204 | configExtension: Partial 205 | ): Config; 206 | ``` 207 | 208 | Helper function to merge multiple config objects. Objects are merged, arrays are concatenated, scalar values are overridden and `undefined` does nothing. The function assumes that both parameters are tailwind-merge config objects and shouldn't be used as a generic merge function. 209 | 210 | ```ts 211 | const customTwMerge = createTailwindMerge(getDefaultConfig, (config) => 212 | mergeConfigs(config, { 213 | classGroups: { 214 | // ↓ Adding new class group 215 | mySpecialClassGroup: [{ special: ["1", "2"] }], 216 | // ↓ Adding value to existing class group 217 | animate: ["animate-magic"], 218 | }, 219 | }) 220 | ); 221 | ``` 222 | 223 | ## `validators` 224 | 225 | ```ts 226 | interface Validators { 227 | isLength(value: string): boolean; 228 | isArbitraryLength(value: string): boolean; 229 | isNumber(value: string): boolean; 230 | isInteger(value: string): boolean; 231 | isPercent(value: string): boolean; 232 | isArbitraryValue(value: string): boolean; 233 | isTshirtSize(value: string): boolean; 234 | isArbitrarySize(value: string): boolean; 235 | isArbitraryPosition(value: string): boolean; 236 | isArbitraryUrl(value: string): boolean; 237 | isArbitraryNumber(value: string): boolean; 238 | isArbitraryShadow(value: string): boolean; 239 | isAny(value: string): boolean; 240 | } 241 | ``` 242 | 243 | An object containing all the validators used in tailwind-merge. They are useful if you want to use a custom config with [`extendTailwindMerge`](#extendtailwindmerge) or [`createTailwindMerge`](#createtailwindmerge). E.g. the `classGroup` for padding is defined as 244 | 245 | ```ts 246 | const paddingClassGroup = [{ p: [validators.isLength] }]; 247 | ``` 248 | 249 | A brief summary for each validator: 250 | 251 | - `isLength` checks whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), a arbitrary length (`[3%]`, `[4px]`, `[length:var(--my-var)]`), or one of the strings `px`, `full` or `screen`. 252 | - `isArbitraryLength` checks for arbitrary length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`). 253 | - `isNumber` checks for numbers (`3`, `1.5`) 254 | - `isArbitraryNumber` checks whether class part is an arbitrary value which starts with `number:` or is a number (`[number:var(--value)]`, `[450]`) which is necessary for font-weight and stroke-width classNames. 255 | - `isInteger` checks for integer values (`3`) and arbitrary integer values (`[3]`). 256 | - `isPercent` checks for percent values (`12.5%`) which is used for color stop positions. 257 | - `isArbitraryValue` checks whether the class part is enclosed in brackets (`[something]`) 258 | - `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`). 259 | - `isArbitrarySize` checks whether class part is an arbitrary value which starts with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames. 260 | - `isArbitraryPosition` checks whether class part is an arbitrary value which starts with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames. 261 | - `isArbitraryUrl` checks whether class part is an arbitrary value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames. 262 | - `isArbitraryShadow` checks whether class part is an arbitrary value which starts with the same pattern as a shadow value (`[0_35px_60px_-15px_rgba(0,0,0,0.3)]`), namely with two lengths separated by a underscore. 263 | - `isAny` always returns true. Be careful with this validator as it might match unwanted classes. I use it primarily to match colors or when I'm certain there are no other class groups in a namespace. 264 | 265 | ## `Config` 266 | 267 | ```ts 268 | interface Config { … } 269 | ``` 270 | 271 | TypeScript type for config object. Useful if you want to build a `createConfig` function but don't want to define it inline in [`extendTailwindMerge`](#extendtailwindmerge) or [`createTailwindMerge`](#createtailwindmerge). 272 | 273 | --- 274 | 275 | Next: [Writing plugins](./writing-plugins.md) 276 | 277 | Previous: [Recipes](./recipes.md) 278 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Basic usage 4 | 5 | If you're using Tailwind CSS without any extra config, you can use [`twMerge`](./api-reference.md#twmerge) right away. You can safely stop reading the documentation here. 6 | 7 | ## Usage with custom Tailwind config 8 | 9 | If you're using a custom Tailwind config, you may need to configure tailwind-merge as well to merge classes properly. 10 | 11 | The default [`twMerge`](./api-reference.md#twmerge) function is configured in a way that you can still use it if all the following points apply to your Tailwind config: 12 | 13 | - Only using color names which don't clash with other Tailwind class names 14 | - Only deviating by number values from number-based Tailwind classes 15 | - Only using font-family classes which don't clash with default font-weight classes 16 | - Sticking to default Tailwind config for everything else 17 | 18 | If some of these points don't apply to you, you can test whether `twMerge` still works as intended with your custom classes. Otherwise, you need create your own custom merge function by either extending the default tailwind-merge config or using a completely custom one. 19 | 20 | The tailwind-merge config is different from the Tailwind config because it's expected to be shipped and run in the browser as opposed to the Tailwind config which is meant to run at build-time. Be careful in case you're using your Tailwind config directly to configure tailwind-merge in your client-side code because that could result in an unnecessarily large bundle size. 21 | 22 | ### Shape of tailwind-merge config 23 | 24 | The tailwind-merge config is an object with a few keys. 25 | 26 | ```ts 27 | const tailwindMergeConfig = { 28 | // ↓ Set how many values should be stored in cache. 29 | cacheSize: 500, 30 | // ↓ Optional prefix from TaiLwind config 31 | prefix: 'tw-', 32 | // ↓ Optional separator from TaiLwind config 33 | separator: '_', 34 | theme: { 35 | // Theme scales are defined here 36 | // This is not the theme object from your Tailwind config 37 | }, 38 | classGroups: { 39 | // Class groups are defined here 40 | }, 41 | conflictingClassGroups: { 42 | // Conflicts between class groups are defined here 43 | }, 44 | conflictingClassGroupModifiers: { 45 | // Conflicts between postfox modifier of a class group and another class group are defined here 46 | }, 47 | } 48 | } 49 | ``` 50 | 51 | ### Class groups 52 | 53 | The library uses a concept of _class groups_ which is an array of Tailwind classes which all modify the same CSS property. E.g. here is the position class group. 54 | 55 | ```ts 56 | const positionClassGroup = [ 57 | "static", 58 | "fixed", 59 | "absolute", 60 | "relative", 61 | "sticky", 62 | ]; 63 | ``` 64 | 65 | tailwind-merge resolves conflicts between classes in a class group and only keeps the last one passed to the merge function call. 66 | 67 | ```ts 68 | twMerge("static sticky relative"); // → 'relative' 69 | ``` 70 | 71 | Tailwind classes often share the beginning of the class name, so elements in a class group can also be an object with values of the same shape as a class group (yes, the shape is recursive). In the object each key is joined with all the elements in the corresponding array with a dash (`-`) in between. 72 | 73 | E.g. here is the overflow class group which results in the classes `overflow-auto`, `overflow-hidden`, `overflow-visible` and `overflow-scroll`. 74 | 75 | ```ts 76 | const overflowClassGroup = [ 77 | { overflow: ["auto", "hidden", "visible", "scroll"] }, 78 | ]; 79 | ``` 80 | 81 | Sometimes it isn't possible to enumerate all elements in a class group. Think of a Tailwind class which allows arbitrary values. In this scenario you can use a validator function which takes a _class part_ and returns a boolean indicating whether a class is part of a class group. 82 | 83 | E.g. here is the fill class group. 84 | 85 | ```ts 86 | const isArbitraryValue = (classPart: string) => /^\[.+\]$/.test(classPart); 87 | const fillClassGroup = [{ fill: ["current", isArbitraryValue] }]; 88 | ``` 89 | 90 | Because the function is under the `fill` key, it will only get called for classes which start with `fill-`. Also, the function only gets passed the part of the class name which comes after `fill-`, this way you can use the same function in multiple class groups. tailwind-merge exports its own [validators](./api-reference.md#validators), so you don't need to recreate them. 91 | 92 | You can use an empty string (`''`) as a class part if you want to indicate that the preceding part was the end. This is useful for defining elements which are marked as `DEFAULT` in the Tailwind config. 93 | 94 | ```ts 95 | // ↓ Resolves to filter and filter-none 96 | const filterClassGroup = [{ filter: ["", "none"] }]; 97 | ``` 98 | 99 | Each class group is defined under its ID in the `classGroups` object in the config. This ID is only used internally, and the only thing that matters is that it is unique among all class groups. 100 | 101 | ### Conflicting class groups 102 | 103 | Sometimes there are conflicts across Tailwind classes which are more complex than "remove all those other classes when a class from this group is present in the class list string". 104 | 105 | One example is the combination of the classes `px-3` (setting `padding-left` and `padding-right`) and `pr-4` (setting `padding-right`). 106 | 107 | If they are passed to `twMerge` as `pr-4 px-3`, you most likely intend to apply `padding-left` and `padding-right` from the `px-3` class and want `pr-4` to be removed, indicating that both these classes should belong to a single class group. 108 | 109 | But if they are passed to `twMerge` as `px-3 pr-4`, you want to set the `padding-right` from `pr-4` but still want to apply the `padding-left` from `px-3`, so `px-3` shouldn't be removed when inserting the classes in this order, indicating they shouldn't be in the same class group. 110 | 111 | To summarize, `px-3` should stand in conflict with `pr-4`, but `pr-4` should not stand in conflict with `px-3`. To achieve this, we need to define asymmetric conflicts across class groups. 112 | 113 | This is what the `conflictingClassGroups` object in the tailwind-merge config is for. You define a key in it which is the ID of a class group which _creates_ a conflict and the value is an array of IDs of class group which _receive_ a conflict. 114 | 115 | ```ts 116 | const conflictingClassGroups = { 117 | px: ["pr", "pl"], 118 | }; 119 | ``` 120 | 121 | If a class group _creates_ a conflict, it means that if it appears in a class list string passed to `twMerge`, all preceding class groups in the string which _receive_ the conflict will be removed. 122 | 123 | When we think of our example, the `px` class group creates a conflict which is received by the class groups `pr` and `pl`. This way `px-3` removes a preceding `pr-4`, but not the other way around. 124 | 125 | ### Postfix modifiers conflicting with class groups 126 | 127 | Tailwind CSS allows postfix modifiers for some classes. E.g. you can set font-size and line-height together with `text-lg/7` with `/7` being the postfix modifier. This means that any line-height classes preceding a font-size class with a modifier should be removed. 128 | 129 | For this tailwind-merge has the `conflictingClassGroupModifiers` object in its config with the same shape as `conflictingClassGroups` explained in the [section above](#conflicting-class-groups). This time the key is the ID of a class group whose modifier _creates_ a conflict and the value is an array of IDs of class groups which _receive_ the conflict. 130 | 131 | ```ts 132 | const conflictingClassGroupModifiers = { 133 | "font-size": ["leading"], 134 | }; 135 | ``` 136 | 137 | ### Theme 138 | 139 | In the Tailwind config you can modify theme scales. tailwind-merge follows the same keys for the theme scales, but doesn't support all of them. tailwind-merge only supports theme scales which are used in multiple class groups to save bundle size (more info to that in [PR 55](https://github.com/compi-ui/tw-merge/pull/55)). At the moment these are: 140 | 141 | - `colors` 142 | - `spacing` 143 | - `blur` 144 | - `brightness` 145 | - `borderColor` 146 | - `borderRadius` 147 | - `borderSpacing` 148 | - `borderWidth` 149 | - `contrast` 150 | - `grayscale` 151 | - `hueRotate` 152 | - `invert` 153 | - `gap` 154 | - `gradientColorStops` 155 | - `gradientColorStopPositions` 156 | - `inset` 157 | - `margin` 158 | - `opacity` 159 | - `padding` 160 | - `saturate` 161 | - `scale` 162 | - `sepia` 163 | - `skew` 164 | - `space` 165 | - `translate` 166 | 167 | If you modified one of these theme scales in your Tailwind config, you can add all your keys right here and tailwind-merge will take care of the rest. If you modified other theme scales, you need to figure out the class group to modify in the [default config](./api-reference.md#getdefaultconfig). 168 | 169 | ### Extending the tailwind-merge config 170 | 171 | If you only need to extend the default tailwind-merge config, [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) is the easiest way to extend the config. You provide it a `configExtension` object which gets [merged](./api-reference.md#mergeconfigs) with the default config. Therefore, all keys here are optional. 172 | 173 | ```ts 174 | import { extendTailwindMerge } from "tailwind-merge"; 175 | 176 | const customTwMerge = extendTailwindMerge({ 177 | // ↓ Add values to existing theme scale or create a new one 178 | theme: { 179 | spacing: ["sm", "md", "lg"], 180 | }, 181 | // ↓ Add values to existing class groups or define new ones 182 | classGroups: { 183 | foo: ["foo", "foo-2", { "bar-baz": ["", "1", "2"] }], 184 | bar: [{ qux: ["auto", (value) => Number(value) >= 1000] }], 185 | baz: ["baz-sm", "baz-md", "baz-lg"], 186 | }, 187 | // ↓ Here you can define additional conflicts across class groups 188 | conflictingClassGroups: { 189 | foo: ["bar"], 190 | }, 191 | // ↓ Define conflicts between postfix modifiers and class groups 192 | conflictingClassGroupModifiers: { 193 | baz: ["bar"], 194 | }, 195 | }); 196 | ``` 197 | 198 | ### Using completely custom tailwind-merge config 199 | 200 | If you need to modify the tailwind-merge config and need more control than [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) gives you or don't want to use the default config (and tree-shake it out of your bundle), you can use [`createTailwindMerge`](./api-reference.md#createtailwindmerge). 201 | 202 | The function takes a callback which returns the config you want to use and returns a custom `twMerge` function. 203 | 204 | ```ts 205 | import { createTailwindMerge } from "tailwind-merge"; 206 | 207 | const customTwMerge = createTailwindMerge(() => ({ 208 | cacheSize: 500, 209 | theme: {}, 210 | classGroups: { 211 | foo: ["foo", "foo-2", { "bar-baz": ["", "1", "2"] }], 212 | bar: [{ qux: ["auto", (value) => Number(value) >= 1000] }], 213 | baz: ["baz-sm", "baz-md", "baz-lg"], 214 | }, 215 | conflictingClassGroups: { 216 | foo: ["bar"], 217 | }, 218 | conflictingClassGroupModifiers: { 219 | baz: ["bar"], 220 | }, 221 | })); 222 | ``` 223 | 224 | The callback passed to `createTailwindMerge` will be called when `customTwMerge` is called the first time, so you don't need to worry about the computations in it affecting app startup performance in case you aren't using tailwind-merge at app startup. 225 | 226 | ### Using tailwind-merge plugins 227 | 228 | You can use both [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) and [`createTailwindMerge`](./api-reference.md#createtailwindmerge) with third-party plugins. Just add them as arguments after your config. 229 | 230 | ```ts 231 | import { extendTailwindMerge, createTailwindMerge } from 'tailwind-merge' 232 | import { withMagic } from 'tailwind-merge-magic-plugin' 233 | import { withMoreMagic } from 'tailwind-merge-more-magic-plugin' 234 | 235 | // With your own config 236 | const twMerge1 = extendTailwindMerge({ … }, withMagic, withMoreMagic) 237 | 238 | // Only using plugin with default config 239 | const twMerge2 = extendTailwindMerge(withMagic, withMoreMagic) 240 | 241 | // Using `createTailwindMerge` 242 | const twMerge3 = createTailwindMerge(() => ({ … }), withMagic, withMoreMagic) 243 | ``` 244 | 245 | ## Ordering 246 | 247 | ```js 248 | function orderByPriority(_targets) { 249 | const targets = [..._targets]; 250 | let seenTargets = []; 251 | let i = 0; 252 | while (i < targets.length) { 253 | const target = targets[i]; 254 | const seenIndex = seenTargets.findIndex((t) => target.startsWith(t)); 255 | if (seenIndex >= 0) { 256 | const [t] = targets.splice(i, 1); 257 | targets.splice(seenIndex, 0, t); 258 | return orderByPriority(targets); 259 | } 260 | seenTargets.push(target); 261 | i++; 262 | continue; 263 | } 264 | return targets; 265 | } 266 | 267 | function orderTargets(targets) { 268 | return orderByPriority(targets.sort()); 269 | } 270 | ``` 271 | 272 | --- 273 | 274 | Next: [Recipes](./recipes.md) 275 | 276 | Previous: [Features](./features.md) 277 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please see [CONTRIBUTING](../.github/CONTRIBUTING.md) for details. 4 | 5 | --- 6 | 7 | Next: [Similar packages](./similar-packages.md) 8 | 9 | Previous: [Versioning](./versioning.md) 10 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | ## Optimized for speed 4 | 5 | - Results get cached by default, so you don't need to worry about wasteful re-renders. The library uses a [LRU cache]() which stores up to 500 different results. The cache size can be modified or opt-out of by using [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge). 6 | - Expensive computations happen upfront so that `twMerge` calls without a cache hit stay fast. 7 | - These computations are called lazily on the first call to `twMerge` to prevent it from impacting app startup performance if it isn't used initially. 8 | 9 | ## Last conflicting class wins 10 | 11 | ```ts 12 | twMerge("p-5 p-2 p-4"); // → 'p-4' 13 | ``` 14 | 15 | ## Allows refinements 16 | 17 | ```ts 18 | twMerge("p-3 px-5"); // → 'p-3 px-5' 19 | twMerge("inset-x-4 right-4"); // → 'inset-x-4 right-4' 20 | ``` 21 | 22 | ## Resolves non-trivial conflicts 23 | 24 | ```ts 25 | twMerge("inset-x-px -inset-1"); // → '-inset-1' 26 | twMerge("bottom-auto inset-y-6"); // → 'inset-y-6' 27 | twMerge("inline block"); // → 'block' 28 | ``` 29 | 30 | ## Supports modifiers and stacked modifiers 31 | 32 | ```ts 33 | twMerge("p-2 hover:p-4"); // → 'p-2 hover:p-4' 34 | twMerge("hover:p-2 hover:p-4"); // → 'hover:p-4' 35 | twMerge("hover:focus:p-2 focus:hover:p-4"); // → 'focus:hover:p-4' 36 | ``` 37 | 38 | ## Supports arbitrary values 39 | 40 | ```ts 41 | twMerge("bg-black bg-[color:var(--mystery-var)]"); // → 'bg-[color:var(--mystery-var)]' 42 | twMerge("grid-cols-[1fr,auto] grid-cols-2"); // → 'grid-cols-2' 43 | ``` 44 | 45 | ## Supports arbitrary properties 46 | 47 | ```ts 48 | twMerge("[mask-type:luminance] [mask-type:alpha]"); // → '[mask-type:alpha]' 49 | twMerge("[--scroll-offset:56px] lg:[--scroll-offset:44px]"); 50 | // → '[--scroll-offset:56px] lg:[--scroll-offset:44px]' 51 | 52 | // Don't do this! 53 | twMerge("[padding:1rem] p-8"); // → '[padding:1rem] p-8' 54 | ``` 55 | 56 | Watch out for mixing arbitrary properties which could be expressed as Tailwind classes. tailwind-merge does not resolve conflicts between arbitrary properties and their matching Tailwind classes to keep the bundle size small. 57 | 58 | ## Supports arbitrary variants 59 | 60 | ```ts 61 | twMerge("[&:nth-child(3)]:py-0 [&:nth-child(3)]:py-4"); // → '[&:nth-child(3)]:py-4' 62 | twMerge("dark:hover:[&:nth-child(3)]:py-0 hover:dark:[&:nth-child(3)]:py-4"); 63 | // → 'hover:dark:[&:nth-child(3)]:py-4' 64 | 65 | // Don't do this! 66 | twMerge("[&:focus]:ring focus:ring-4"); // → '[&:focus]:ring focus:ring-4' 67 | ``` 68 | 69 | Similarly to arbitrary properties, tailwind-merge does not resolve conflicts between arbitrary variants and their matching predefined modifiers for bundle size reasons. 70 | 71 | ## Supports important modifier 72 | 73 | ```ts 74 | twMerge("!p-3 !p-4 p-5"); // → '!p-4 p-5' 75 | twMerge("!right-2 !-inset-x-1"); // → '!-inset-x-1' 76 | ``` 77 | 78 | ## Supports postfix modifiers 79 | 80 | ```ts 81 | twMerge("text-sm leading-6 text-lg/7"); // → 'text-lg/7' 82 | ``` 83 | 84 | ## Preserves non-Tailwind classes 85 | 86 | ```ts 87 | twMerge("p-5 p-2 my-non-tailwind-class p-4"); // → 'my-non-tailwind-class p-4' 88 | ``` 89 | 90 | ## Supports custom colors out of the box 91 | 92 | ```ts 93 | twMerge("text-red text-secret-sauce"); // → 'text-secret-sauce' 94 | ``` 95 | 96 | Why no object support? [Read here](https://github.com/compi-ui/tw-merge/discussions/137#discussioncomment-3481605). 97 | 98 | --- 99 | 100 | Next: [Configuration](./configuration.md) 101 | 102 | Previous: [What is it for](./what-is-it-for.md) 103 | -------------------------------------------------------------------------------- /docs/recipes.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | How to configure tailwind-merge with some common patterns. 4 | 5 | ## Adding custom scale from Tailwind config to tailwind-merge config 6 | 7 | > I have a custom shadow scale with the keys 100, 200 and 300 configured in Tailwind. How do I make tailwind-merge resolve conflicts among those? 8 | 9 | You'll be able to do this by creating a custom `twMerge` functon with [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge). 10 | 11 | First, check whether your particular theme scale is included in tailwind-merge's theme config object [here](./configuration.md#theme). In the hypothetical case that tailwind-merge supported Tailwind's `boxShadow` theme scale, you could add it to the tailwind-merge config like this: 12 | 13 | ```js 14 | const customTwMerge = extendTailwindMerge({ 15 | theme: { 16 | // The `boxShadow` key isn't actually supported 17 | boxShadow: [{ shadow: ["100", "200", "300"] }], 18 | }, 19 | }); 20 | ``` 21 | 22 | In the case of the `boxShadow` scale, tailwind-merge doesn't include it in the theme object. Instead, we need to check out the [default config of tailwind-merge](../src/lib/default-config.ts) and search for the class group ID of the box shadow scale. After a quick search we find that tailwind-merge is using the key `shadow` for that group. We can add our custom classes to that group like this: 23 | 24 | ```js 25 | const customTwMerge = extendTailwindMerge({ 26 | classGroups: { 27 | shadow: [{ shadow: ["100", "200", "300"] }], 28 | }, 29 | }); 30 | ``` 31 | 32 | Note that by using `extendTailwindMerge` we're only adding our custom classes to the existing ones in the config, so `twMerge('shadow-200 shadow-lg')` will return the string `shadow-lg`. In most cases that's fine because you won't use that class in your project. 33 | 34 | If you expect classes like `shadow-lg` to be input in `twMerge` and don't want the class to cause incorrect merges, you can explicitly override the class group with [`createTailwindMerge`](./api-reference.md#createtailwindmerge), removing the default classes. 35 | 36 | ```js 37 | const customTwMerge = createTailwindMerge(() => { 38 | const config = getDefaultConfig(); 39 | config.classGroups.shadow = [{ shadow: ["100", "200", "300"] }]; 40 | return config; 41 | }); 42 | ``` 43 | 44 | ## Extracting classes with Tailwind's [`@apply`](https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply) 45 | 46 | > How do I make tailwind-merge resolve conflicts with a custom class created with `@apply`? 47 | > 48 | > ```css 49 | > .btn-primary { 50 | > @apply py-2 px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-700; 51 | > } 52 | > ``` 53 | 54 | I don't recommend using Tailwind's `@apply` directive for classes that might get processed with tailwind-merge. 55 | 56 | tailwind-merge would need to be configured so that it knows about which classes `.btn-primary` is in conflict with. This means: If someone adds another Tailwind class to the `@apply` directive, the tailwind-merge config would need to get modified accordignly, keeping it in sync with the written CSS. This easy-to-miss dependency is fragile and can lead to bugs with incorrect merging behavior. 57 | 58 | Instead of creating custom CSS classes, I recommend keeping the collection of Tailwind classes in a string variable in JavaScript and access it whenever you want to apply those styles. This way you can reuse the collection of styles but don't need to touch the tailwind-merge config. 59 | 60 | ```jsx 61 | // React components with JSX syntax used in this example 62 | 63 | const BTN_PRIMARY_CLASSNAMES = 64 | "py-2 px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-700"; 65 | 66 | function ButtonPrimary(props) { 67 | return ( 68 |