├── .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 |
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 |
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 |
72 | );
73 | }
74 | ```
75 |
76 | ## Modifying inputs and output of `twMerge`
77 |
78 | > How do I make `twMerge` accept the same argument types as clsx/classnames?
79 |
80 | You can wrap `twMerge` in another function which can modify the inputs and/or output.
81 |
82 | ```js
83 | function customTwMerge(...inputs) {
84 | const modifiedInputs = modifyInputs(inputs);
85 | return twMerge(modifiedInputs);
86 | }
87 | ```
88 |
89 | ---
90 |
91 | Next: [API reference](./api-reference.md)
92 |
93 | Previous: [Configuration](./configuration.md)
94 |
--------------------------------------------------------------------------------
/docs/similar-packages.md:
--------------------------------------------------------------------------------
1 | # Similar packages
2 |
3 | ## TypeScript/JavaScript
4 |
5 | - [@robit-dev/tailwindcss-class-combiner](https://www.npmjs.com/package/@robit-dev/tailwindcss-class-combiner)
6 | - [tailshake](https://www.npmjs.com/package/tailshake)
7 | - [tailwind-classlist](https://www.npmjs.com/package/tailwind-classlist)
8 | - [tailwind-override](https://www.npmjs.com/package/tailwind-override)
9 |
10 | ## Other languages
11 |
12 | - [tailwind_merge](https://rubygems.org/gems/tailwind_merge) (Ruby)
13 | - [Twix](https://hex.pm/packages/twix/0.1.0) (Elixir)
14 |
15 | ---
16 |
17 | Previous: [Contributing](./contributing.md)
18 |
--------------------------------------------------------------------------------
/docs/versioning.md:
--------------------------------------------------------------------------------
1 | # Versioning
2 |
3 | This package follows the [SemVer](https://semver.org) versioning rules. More specifically:
4 |
5 | - Patch version gets incremented when unintended behavior is fixed, which doesn't break any existing API. Note that bug fixes can still alter which styles are applied. E.g. a bug gets fixed in which the conflicting classes `inline` and `block` weren't merged correctly so that both would end up in the result.
6 |
7 | - Minor version gets incremented when additional features are added which don't break any existing API. However, a minor version update might still alter which styles are applied if you use Tailwind features not yet supported by tailwind-merge. E.g. a new Tailwind prefix `magic` gets added to this package which changes the result of `twMerge('magic:px-1 magic:p-3')` from `magic:px-1 magic:p-3` to `magic:p-3`.
8 |
9 | - Major version gets incremented when breaking changes are introduced to the package API. E.g. the return type of `twMerge` changes.
10 |
11 | - `alpha` releases might introduce breaking changes on any update. Whereas `beta` releases only introduce new features or bug fixes.
12 |
13 | - Releases with major version 0 might introduce breaking changes on a minor version update.
14 |
15 | - A non-production-ready version of every commit pushed to the main branch is released under the `dev` tag for testing purposes. It has a format like [`1.6.1-dev.4202ccf913525617f19fbc493db478a76d64d054`](https://www.npmjs.com/package/tailwind-merge/v/1.6.1-dev.4202ccf913525617f19fbc493db478a76d64d054) in which the first numbers are the corresponding last release and the hash at the end is the git SHA of the commit. You can install the latest dev release with `yarn add tailwind-merge@dev`.
16 |
17 | - A changelog is documented in [GitHub Releases](https://github.com/compi-ui/tw-merge/releases).
18 |
19 | ---
20 |
21 | Next: [Contributing](./contributing.md)
22 |
23 | Previous: [Writing plugins](./writing-plugins.md)
24 |
--------------------------------------------------------------------------------
/docs/what-is-it-for.md:
--------------------------------------------------------------------------------
1 | # What is it for
2 |
3 | If you use Tailwind with a component-based UI renderer like [React](https://reactjs.org) or [Vue](https://vuejs.org), you're probably familiar with the situation that you want to change some styles of a component, but only in one place.
4 |
5 | ```jsx
6 | // React components with JSX syntax used in this example
7 |
8 | function MyGenericInput(props) {
9 | const className = `border rounded px-2 py-1 ${props.className || ""}`;
10 | return ;
11 | }
12 |
13 | function MySlightlyModifiedInput(props) {
14 | return (
15 |
19 | );
20 | }
21 | ```
22 |
23 | When the `MySlightlyModifiedInput` is rendered, an input with the className `border rounded px-2 py-1 p-3` gets created. But because of the way the [CSS cascade](https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade) works, the styles of the `p-3` class are ignored. The order of the classes in the `className` string doesn't matter at all and the only way to apply the `p-3` styles is to remove both `px-2` and `py-1`.
24 |
25 | This is where tailwind-merge comes in.
26 |
27 | ```jsx
28 | function MyGenericInput(props) {
29 | // ↓ Now `props.className` can override conflicting classes
30 | const className = twMerge("border rounded px-2 py-1", props.className);
31 | return ;
32 | }
33 | ```
34 |
35 | tailwind-merge overrides conflicting classes and keeps everything else untouched. In the case of the `MySlightlyModifiedInput`, the input now only renders the classes `border rounded p-3`.
36 |
37 | ---
38 |
39 | Next: [Features](./features.md)
40 |
41 | Previous: [Overview](../README.md)
42 |
--------------------------------------------------------------------------------
/docs/writing-plugins.md:
--------------------------------------------------------------------------------
1 | # Writing plugins
2 |
3 | This library supports classes of the core Tailwind library out of the box, but not classes of any plugins. But it's possible and hopefully easy to write third-party plugins for tailwind-merge. In case you want to write a plugin, I invite you to follow these steps:
4 |
5 | - Create a package called `tailwind-merge-magic-plugin` with tailwind-merge as peer dependency which exports a function `withMagic` and replace "magic" with your plugin name.
6 | - This function would be ideally a `createConfig` function which takes a config object as argument and returns the modified config object.
7 | - If you create new class groups, prepend them with `magic.` (your plugin name with a dot at the end) so they don't collide with class group names from other plugins or even future class groups in tailwind-merge itself.
8 | - Use the [`validators`](./api-reference.md#validators) and [`mergeConfigs`](./api-reference.md#mergeconfigs) from tailwind-merge to extend the config with magic.
9 |
10 | Here is an example of how a plugin could look like:
11 |
12 | ```ts
13 | import { mergeConfigs, validators, Config } from "tailwind-merge";
14 |
15 | export function withMagic(config: Config): Config {
16 | return mergeConfigs(config, {
17 | classGroups: {
18 | "magic.my-group": [{ magic: [validators.isLength, "wow"] }],
19 | },
20 | });
21 | }
22 | ```
23 |
24 | This plugin can then be used like this:
25 |
26 | ```ts
27 | import { extendTailwindMerge } from "tailwind-merge";
28 | import { withMagic } from "tailwind-merge-magic-plugin";
29 |
30 | const twMerge = extendTailwindMerge(withMagic);
31 | ```
32 |
33 | Also, feel free to check out [tailwind-merge-rtl-plugin](https://www.npmjs.com/package/tailwind-merge-rtl-plugin) as a real example of a tailwind-merge plugin.
34 |
35 | ---
36 |
37 | Next: [Versioning](./versioning.md)
38 |
39 | Previous: [API reference](./api-reference.md)
40 |
--------------------------------------------------------------------------------
/dts.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // This function will run for each entry/format/env combination
3 | rollup(config, options) {
4 | if (options.format === "esm") {
5 | config = {
6 | ...config,
7 | preserveModules: true,
8 | };
9 |
10 | config.output = {
11 | ...config.output,
12 | dir: "dist",
13 | entryFileNames: "[name].mjs",
14 | };
15 |
16 | delete config.output.file;
17 | }
18 |
19 | return config;
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tw-merge",
3 | "version": "0.0.1-alpha.3",
4 | "description": "Merge CSS utility classes without style conflicts - small and zero config",
5 | "keywords": [
6 | "tailwindcss",
7 | "tailwind",
8 | "css",
9 | "classes",
10 | "className",
11 | "classList",
12 | "merge",
13 | "conflict",
14 | "override",
15 | "utility"
16 | ],
17 | "homepage": "https://github.com/compi-ui/tw-merge",
18 | "bugs": {
19 | "url": "https://github.com/compi-ui/tw-merge/issues"
20 | },
21 | "license": "MIT",
22 | "author": "Dani Guardiola",
23 | "files": [
24 | "dist",
25 | "src"
26 | ],
27 | "source": "src/index.ts",
28 | "exports": {
29 | "types": "./dist/index.d.ts",
30 | "require": "./dist/index.js",
31 | "import": "./dist/tw-merge.mjs",
32 | "default": "./dist/tw-merge.mjs"
33 | },
34 | "module": "dist/tw-merge.mjs",
35 | "main": "dist/index.js",
36 | "types": "./dist/index.d.ts",
37 | "repository": {
38 | "type": "git",
39 | "url": "https://github.com/compi-ui/tw-merge.git"
40 | },
41 | "sideEffects": false,
42 | "scripts": {
43 | "build": "dts build",
44 | "test": "dts test",
45 | "test:exports": "node scripts/test-built-package-exports.js && node scripts/test-built-package-exports.mjs",
46 | "lint": "eslint --max-warnings 0 '**'",
47 | "size": "size-limit",
48 | "gen-tailwind-rules": "tsx scripts/gen-tailwind-rules.ts"
49 | },
50 | "devDependencies": {
51 | "@size-limit/preset-small-lib": "^8.2.4",
52 | "@types/jest": "^29.5.0",
53 | "@typescript-eslint/eslint-plugin": "^5.57.0",
54 | "@typescript-eslint/parser": "^5.57.0",
55 | "dts-cli": "^1.6.3",
56 | "eslint": "^8.37.0",
57 | "eslint-plugin-import": "^2.27.5",
58 | "eslint-plugin-jest": "^27.2.1",
59 | "globby": "^11.1.0",
60 | "prettier": "^2.8.7",
61 | "size-limit": "^8.2.4",
62 | "tailwind-merge": "^1.12.0",
63 | "ts-jest": "^29.0.5",
64 | "ts-toolbelt": "^9.6.0",
65 | "tsx": "^3.12.7",
66 | "typescript": "^5.0.3"
67 | },
68 | "publishConfig": {
69 | "provenance": true
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/scripts/gen-benchmark-data.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 |
4 | const docsFiles = fs
5 | .readdirSync(path.join(__dirname, "../docs"))
6 | .map((file) => path.join("docs", file));
7 | const testFiles = fs
8 | .readdirSync(path.join(__dirname, "../tests"))
9 | .map((file) => path.join("tests", file));
10 | const files = [...docsFiles, ...testFiles];
11 |
12 | const twMergeInputRegex = /twMerge\("(?.*)"/g;
13 |
14 | const inputs = files
15 | .map((file) => {
16 | const fileContent = fs.readFileSync(path.join(__dirname, `../${file}`), {
17 | encoding: "utf-8",
18 | });
19 | return Array.from(fileContent.matchAll(twMergeInputRegex)).map(
20 | (match) => match.groups!.input
21 | );
22 | })
23 | .flat();
24 |
25 | const allClasses = inputs.map((input) => input.split(" ")).flat();
26 |
27 | const randomInputs = Array.from({ length: 200 }, () => {
28 | const classes = Array.from({ length: 10 }, () => {
29 | const index = Math.floor(Math.random() * allClasses.length);
30 | return allClasses[index];
31 | });
32 | return classes.join(" ");
33 | });
34 |
35 | fs.writeFileSync(
36 | path.join(__dirname, "../benchmark/_generated/inputs.json"),
37 | JSON.stringify([...inputs, ...randomInputs], null, 2)
38 | );
39 |
--------------------------------------------------------------------------------
/scripts/gen-tailwind-rules.ts:
--------------------------------------------------------------------------------
1 | import { generateTailwindRuleSet } from "../src/generate-tailwind-rule-set";
2 | generateTailwindRuleSet("all", { importPath: "./rules" });
3 |
--------------------------------------------------------------------------------
/scripts/test-built-package-exports.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 |
3 | const { twMerge } = require("..");
4 |
5 | assert(twMerge("") === "");
6 | assert(
7 | twMerge("px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]") ===
8 | "hover:bg-dark-red p-3 bg-[#B91C1C]"
9 | );
10 |
11 | console.log("[tw-merge] Tests for built CJS package exports passed.");
12 |
--------------------------------------------------------------------------------
/scripts/test-built-package-exports.mjs:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 |
3 | // Not ideal, but there seems to be no way to point the import resolver to the package.json file if this isn't a npm package.
4 | import { twMerge } from "../dist/tw-merge.mjs";
5 |
6 | assert(twMerge("") === "");
7 | assert(
8 | twMerge("px-2 py-1 bg-red hover:bg-dark-red p-3 bg-[#B91C1C]") ===
9 | "hover:bg-dark-red p-3 bg-[#B91C1C]"
10 | );
11 |
12 | console.log("[tw-merge] Tests for built ESM package exports passed.");
13 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/data.ts:
--------------------------------------------------------------------------------
1 | export const CONSTANTS = {
2 | DISPLAY:
3 | "block|inline-block|inline-flex|inline-table|inline-grid|inline|flex|table-caption|table-cell|table-column-group|table-column|table-footer-group|table-header-group|table-row-group|table-row|table|flow-root|grid|contents|list-item|hidden",
4 | ISOLATION: "isolate|isolation-auto",
5 | OBJECT_FIT: "contain|cover|fill|none|scale-down",
6 | BG_AND_OBJECT_POSITION:
7 | "bottom|center|left|left-bottom|left-top|right|right-bottom|right-top|top",
8 | POSITION: "static|fixed|absolute|relative|sticky",
9 | VISIBILITY: "visible|invisible|collapse",
10 | FLEX_DIRECTION: "row|row-reverse|col|col-reverse",
11 | FLEX_WRAP: "wrap|wrap-reverse|nowrap",
12 | ALIGN_CONTENT:
13 | "normal|center|start|end|between|around|evenly|baseline|stretch",
14 | FONT_AND_SHADOW_SIZE:
15 | "xs|sm|base|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|inner|none",
16 | FONT_SMOOTHING: "antialiased|subpixel-antialiased",
17 | FONT_STYLE: "italic|not-italic",
18 | FONT_WEIGHT:
19 | "thin|extralight|light|normal|medium|semibold|bold|extrabold|black",
20 | LIST_STYLE_POSITION: "inside|outside",
21 | TEXT_ALIGN: "left|center|right|justify|start|end",
22 | TEXT_DECORATION: "underline|overline|line-through|no-underline",
23 | TEXT_DECORATION_STYLE: "solid|double|dotted|dashed|wavy",
24 | TEXT_TRANSFORM: "uppercase|lowercase|capitalize|normal-case",
25 | TEXT_OVERFLOW: "truncate|text-ellipsis|text-clip",
26 | BG_ATTACHMENT: "fixed|local|scroll",
27 | BG_REPEAT: "repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space",
28 | BG_SIZE: "auto|cover|contain",
29 | BORDER_AND_OUTLINE_STYLE: "solid|dashed|dotted|double|hidden|none",
30 | FVN_FIGURE: "lining-nums|oldstyle-nums",
31 | FVN_SPACING: "proportional-nums|tabular-nums",
32 | FVN_FRACTION: "diagonal-fractions|stacked-fractions",
33 | SCROLL_BEHAVIOR: "auto|smooth",
34 | SCROLL_SNAP_ALIGN: "start|end|center|none",
35 | SCROLL_SNAP_STOP: "normal|always",
36 | SCROLL_SNAP_TYPE: "none|x|y|both|mandatory|proximity",
37 | } as const;
38 |
39 | export const TOP_CONFLICT_RULE = {
40 | "inset-x": "left|right",
41 | "inset-y": "top|bottom",
42 | inset: "inset-x|inset-y|start|end|left|right|top|bottom",
43 | "sr-only": "not-sr-only",
44 | "not-sr-only": "sr-only",
45 | "normal-nums":
46 | "ordinal|slashed-zero|lining-nums|oldstyle-nums|proportional-nums|tabular-nums|diagonal-fractions|stacked-fractons",
47 | ordinal: "normal-nums",
48 | "slashed-zero": "normal-nums",
49 | "lining-nums": "normal-nums",
50 | "oldstyle-nums": "normal-nums",
51 | "proportional-nums": "normal-nums",
52 | "tabular-nums": "normal-nums",
53 | "diagonal-fractions": "normal-nums",
54 | "stacked-fractons": "normal-nums",
55 | "bg-gradient": "bg-none",
56 | "bg-none": "bg-gradient",
57 | } as const;
58 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/gen-file.ts:
--------------------------------------------------------------------------------
1 | import { CONSTANTS, TOP_CONFLICT_RULE } from "./data";
2 | import { GenerationState } from "./generation-state";
3 | import { GenerateTailwindRuleSetOptions } from "./types";
4 |
5 | function orderByPriority(_targets: string[]): string[] {
6 | const targets = [..._targets];
7 | let seenTargets = [];
8 | let i = 0;
9 | while (i < targets.length) {
10 | const target = targets[i] as string;
11 | const seenIndex = seenTargets.findIndex((t) => target.startsWith(t));
12 | if (seenIndex >= 0) {
13 | const [t] = targets.splice(i, 1);
14 | targets.splice(seenIndex, 0, t as string);
15 | return orderByPriority(targets);
16 | }
17 | seenTargets.push(target);
18 | i++;
19 | continue;
20 | }
21 | return targets;
22 | }
23 |
24 | function orderSimpleTargets(targets: string[]) {
25 | return orderByPriority(targets.sort());
26 | }
27 |
28 | function objectFitPositionToConstantName(value: string) {
29 | return {
30 | fit: "OBJECT_FIT",
31 | position: "BG_AND_OBJECT_POSITION",
32 | }[value];
33 | }
34 |
35 | export function generateFile(
36 | state: GenerationState,
37 | {
38 | importPath = "merge-utility",
39 | exportName = "tailwindRuleSet",
40 | }: GenerateTailwindRuleSetOptions
41 | ) {
42 | let file = "";
43 |
44 | // imports
45 | file += "import {\n RuleSet,\n ";
46 | file += state.imports.join(",\n ");
47 | file += `\n} from '${importPath}'\n`;
48 |
49 | // constants
50 | state.constants.forEach((constant) => {
51 | file += `\nconst ${constant} = "${CONSTANTS[constant]}"`;
52 | });
53 | if (state.constants.length > 0) file += "\n";
54 |
55 | // header
56 | file += `\nexport const ${exportName}: RuleSet = [`;
57 |
58 | // top conflict rules
59 | if (state.topConflictRule.length > 0) {
60 | file += `\n conflictRule({\n`;
61 | state.topConflictRule.forEach((rule) => {
62 | file += ` '${rule}': '${TOP_CONFLICT_RULE[rule]}',\n`;
63 | });
64 | file += ` }),`;
65 | }
66 |
67 | // flex direction wrap unique rules
68 | // TODO
69 |
70 | // flex basis grow shrink conflict rule
71 | // TODO
72 |
73 | // text align size unique rules
74 | // TODO
75 |
76 | // bg unique rules
77 | // TODO
78 |
79 | // scroll unique rules
80 | // TODO
81 |
82 | // top unique rules
83 | if (state.topUniqueRules.length > 0) {
84 | file += "\n ...uniqueRules([";
85 | state.topUniqueRules.forEach((rule) => {
86 | file += `\n ${rule},`;
87 | });
88 | file += "\n ]),";
89 | }
90 |
91 | // align content unique rule
92 | // TODO
93 |
94 | // list style position unique rule
95 | // TODO
96 |
97 | // text decoration style unique rule
98 | // TODO
99 |
100 | // border style unique rule
101 | // TODO
102 |
103 | // divide style unique rule
104 | // TODO
105 |
106 | // outline style unique rule
107 | // TODO
108 |
109 | // shadow unique rule
110 | // TODO
111 |
112 | // font weight unique rule
113 | // TODO
114 |
115 | // top simple rule
116 | if (state.topSimpleRule.length > 0) {
117 | file += "\n simpleRule(";
118 | const targets = orderSimpleTargets(state.topSimpleRule);
119 | file += `\n '${targets.join("|")}'`;
120 | file += "\n ),";
121 | }
122 |
123 | // top simple rule by type
124 | if (state.topSimpleRuleByType.length > 0) {
125 | file += "\n simpleRule(";
126 | const targets = orderSimpleTargets(state.topSimpleRuleByType);
127 | file += `\n '${targets.join("|")}'`;
128 | file += "\n ),";
129 | }
130 |
131 | // border cardinal rule
132 | // TODO
133 |
134 | // xy cardinal rules
135 | if (state.xyCardinalRules.length > 0) {
136 | file += `\n ...cardinalRules('${state.xyCardinalRules.join("|")}'),`;
137 | }
138 |
139 | // trbl cardinal rules
140 | // TODO
141 |
142 | // object fit position unique rules
143 | if (state.objectFitPositionUniqueRules.length > 0) {
144 | file += `\n ...uniqueRules([${state.objectFitPositionUniqueRules
145 | .map(objectFitPositionToConstantName)
146 | .join(", ")}], { prefix: 'object' }),`;
147 | }
148 |
149 | // arbitrary rule
150 | if (state.arbitraryRule) file += "\n arbitraryRule(),";
151 |
152 | // footer
153 | file += "\n]\n";
154 |
155 | return file;
156 | }
157 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/gen.ts:
--------------------------------------------------------------------------------
1 | import { UTILITIES_BY_CATEGORY } from "./utilities-by-category";
2 | import { processUtility } from "./process-utility";
3 | import { generateFile } from "./gen-file";
4 | import {
5 | GenerateTailwindRuleSetOptions,
6 | ResolvedRules,
7 | TailwindRules,
8 | } from "./types";
9 | import { GenerationState, EMPTY_GENERATION_STATE } from "./generation-state";
10 |
11 | function resolveCategory(
12 | category: keyof Omit,
13 | config:
14 | | boolean
15 | | (Record & { mode?: "whitelist" | "blacklist" }),
16 | defaultMode: "blacklist" | "whitelist"
17 | ): string[] {
18 | if (typeof config === "boolean") {
19 | if (config) return UTILITIES_BY_CATEGORY[category] as any;
20 | return [];
21 | }
22 |
23 | const { mode = defaultMode, ...utilities } = config;
24 |
25 | if (mode === "whitelist")
26 | return Object.entries(utilities)
27 | .map(([utility, enabled]) => {
28 | if (enabled) return utility;
29 | return false;
30 | })
31 | .filter((value: T | false): value is T => Boolean(value));
32 |
33 | const allUtilities: string[] = UTILITIES_BY_CATEGORY[category] as any;
34 | return allUtilities.filter((utility) => utilities[utility] !== false);
35 | }
36 |
37 | function resolveRules({
38 | mode = "blacklist",
39 | ...categories
40 | }: TailwindRules): ResolvedRules {
41 | const baseResolvedRules: ResolvedRules = Object.fromEntries(
42 | Object.keys(UTILITIES_BY_CATEGORY).map((category) => [category, []])
43 | ) as any;
44 |
45 | if (mode === "whitelist")
46 | return {
47 | ...baseResolvedRules,
48 | ...Object.fromEntries(
49 | Object.entries(categories).map(([category, config]) => [
50 | category,
51 | resolveCategory(category as any, config as any, mode),
52 | ])
53 | ),
54 | };
55 |
56 | return {
57 | ...baseResolvedRules,
58 | ...Object.fromEntries(
59 | Object.entries(UTILITIES_BY_CATEGORY).map(([_category, utilities]) => {
60 | const category = _category as keyof typeof UTILITIES_BY_CATEGORY;
61 | if (categories[category] == null) return [category, utilities] as any;
62 | return [
63 | category,
64 | resolveCategory(category, categories[category] as any, mode),
65 | ];
66 | })
67 | ),
68 | };
69 | }
70 |
71 | function updateImports(state: GenerationState) {
72 | const importConditions: Record<
73 | GenerationState["imports"][number],
74 | unknown[]
75 | > = {
76 | uniqueRule: [
77 | state.alignContentUniqueRule,
78 | state.listStylePositionUniqueRule,
79 | state.textDecorationStyleUniqueRule,
80 | state.borderStyleUniqueRule,
81 | state.divideStyleUniqueRule,
82 | state.outlineStyleUniqueRule,
83 | state.shadowUniqueRule,
84 | state.fontWeightUniqueRule,
85 | ],
86 | uniqueRules: [
87 | state.flexDirectionWrapUniqueRules.length > 0,
88 | state.textAlignSizeUniqueRules.length > 0,
89 | state.bgUniqueRules.length > 0,
90 | state.scrollUniqueRules.length > 0,
91 | state.topUniqueRules.length > 0,
92 | state.objectFitPositionUniqueRules.length > 0,
93 | ],
94 | simpleRule: [
95 | state.topSimpleRule.length > 0,
96 | state.topSimpleRuleByType.length > 0,
97 | ],
98 | cardinalRules: [
99 | state.xyCardinalRules.length > 0,
100 | state.trblCardinalRules.length > 0,
101 | ],
102 | cardinalRule: [state.borderCardinalRule],
103 | arbitraryRule: [state.arbitraryRule],
104 | conflictRule: [
105 | state.topConflictRule.length > 0,
106 | state.flexBasisGrowShrinkConflictRule,
107 | ],
108 | };
109 |
110 | Object.entries(importConditions).forEach(([importName, conditions]) => {
111 | if (conditions.some(Boolean))
112 | state.imports.push(importName as GenerationState["imports"][number]);
113 | });
114 | }
115 |
116 | export function generateTailwindRuleSet(
117 | _rules: TailwindRules | "all",
118 | options: GenerateTailwindRuleSetOptions = {}
119 | ) {
120 | const rules = _rules === "all" ? {} : _rules;
121 | const { target = "console" } = options;
122 | const resolvedRules = resolveRules(rules);
123 | const state = EMPTY_GENERATION_STATE;
124 |
125 | Object.entries(resolvedRules).forEach(([_category, utilities]) => {
126 | const category = _category as keyof ResolvedRules;
127 | for (const utility of utilities) {
128 | processUtility(state, category, utility);
129 | }
130 | });
131 |
132 | updateImports(state);
133 |
134 | const file = generateFile(state, options);
135 |
136 | // eslint-disable-next-line no-console
137 | if (target === "console") console.log(file);
138 | // eslint-disable-next-line no-console
139 | else console.log("TODO");
140 |
141 | return file;
142 | }
143 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/generation-state.ts:
--------------------------------------------------------------------------------
1 | import { CONSTANTS, TOP_CONFLICT_RULE } from "./data";
2 |
3 | export type GenerationState = {
4 | imports: (
5 | | "uniqueRule"
6 | | "simpleRule"
7 | | "cardinalRules"
8 | | "cardinalRule"
9 | | "arbitraryRule"
10 | | "uniqueRules"
11 | | "conflictRule"
12 | )[];
13 | constants: (keyof typeof CONSTANTS)[];
14 | topConflictRule: (keyof typeof TOP_CONFLICT_RULE)[];
15 | flexDirectionWrapUniqueRules: ("direction" | "wrap")[];
16 | flexBasisGrowShrinkConflictRule: ("basis" | "grow" | "shrink")[];
17 | textAlignSizeUniqueRules: ("align" | "size")[];
18 | bgUniqueRules: ("attachment" | "position" | "repeat" | "size")[];
19 | scrollUniqueRules: ("behavior" | "snap-align" | "snap-stop" | "snap-type")[];
20 | topUniqueRules: (
21 | | "DISPLAY"
22 | | "ISOLATION"
23 | | "POSITION"
24 | | "VISIBILITY"
25 | | "FONT_SMOOTHING"
26 | | "FONT_STYLE"
27 | | "FVN_FIGURE"
28 | | "FVN_SPACING"
29 | | "FVN_FRACTION"
30 | | "TEXT_DECORATION"
31 | | "TEXT_TRANSFORM"
32 | | "TEXT_OVERFLOW"
33 | )[];
34 | alignContentUniqueRule: boolean;
35 | listStylePositionUniqueRule: boolean;
36 | textDecorationStyleUniqueRule: boolean;
37 | borderStyleUniqueRule: boolean;
38 | divideStyleUniqueRule: boolean;
39 | outlineStyleUniqueRule: boolean;
40 | shadowUniqueRule: boolean;
41 | fontWeightUniqueRule: boolean;
42 | topSimpleRule: (
43 | | "accent"
44 | | "align"
45 | | "animate"
46 | | "aspect"
47 | | "auto-cols"
48 | | "auto-rows"
49 | | "backdrop-blur"
50 | | "backdrop-brightness"
51 | | "backdrop-contrast"
52 | | "backdrop-grayscale"
53 | | "backdrop-hue-rotate"
54 | | "backdrop-invert"
55 | | "backdrop-opacity"
56 | | "backdrop-saturate"
57 | | "backdrop-sepia"
58 | | "basis"
59 | | "bg-blend"
60 | | "bg-clip"
61 | | "bg-origin"
62 | | "bg-none"
63 | | "bg-gradient"
64 | | "bg"
65 | | "blur"
66 | | "border-collapse"
67 | | "border-spacing"
68 | | "bottom"
69 | | "box-decoration"
70 | | "box"
71 | | "break-after"
72 | | "break-before"
73 | | "break-inside"
74 | | "break"
75 | | "brightness"
76 | | "caption"
77 | | "caret"
78 | | "clear"
79 | | "col-end"
80 | | "col-start"
81 | | "columns"
82 | | "col"
83 | | "container"
84 | | "content"
85 | | "contrast"
86 | | "cursor"
87 | | "decoration"
88 | | "delay"
89 | | "divide-x-reverse"
90 | | "divide-x"
91 | | "divide-y-reverse"
92 | | "divide-y"
93 | | "divide"
94 | | "drop-shadow"
95 | | "duration"
96 | | "ease"
97 | | "end"
98 | | "fill"
99 | | "flex"
100 | | "float"
101 | | "grayscale"
102 | | "grid-cols"
103 | | "grid-flow"
104 | | "grid-rows"
105 | | "grow"
106 | | "hue-rotate"
107 | | "hyphens"
108 | | "h"
109 | | "indent"
110 | | "invert"
111 | | "items"
112 | | "justify-items"
113 | | "justify-self"
114 | | "justify"
115 | | "leading"
116 | | "left"
117 | | "line-clamp"
118 | | "list-image"
119 | | "list"
120 | | "max-h"
121 | | "max-w"
122 | | "min-h"
123 | | "min-w"
124 | | "mix-blend"
125 | | "opacity"
126 | | "order"
127 | | "origin"
128 | | "outline-offset"
129 | | "place-content"
130 | | "place-items"
131 | | "place-self"
132 | | "pointer-events"
133 | | "resize"
134 | | "right"
135 | | "ring-inset"
136 | | "rotate"
137 | | "row-end"
138 | | "row-start"
139 | | "row"
140 | | "saturate"
141 | | "select"
142 | | "self"
143 | | "sepia"
144 | | "shadow"
145 | | "shrink"
146 | | "skew-x"
147 | | "skew-y"
148 | | "space-x-reverse"
149 | | "space-x"
150 | | "space-y-reverse"
151 | | "space-y"
152 | | "start"
153 | | "table"
154 | | "top"
155 | | "touch"
156 | | "tracking"
157 | | "transition"
158 | | "translate-x"
159 | | "translate-y"
160 | | "underline-offset"
161 | | "whitespace"
162 | | "will-change"
163 | | "w"
164 | | "z"
165 | )[];
166 | topSimpleRuleByType: (
167 | | "text"
168 | | "outline"
169 | | "ring-offset"
170 | | "ring"
171 | | "from"
172 | | "via"
173 | | "to"
174 | | "stroke"
175 | | "font"
176 | )[];
177 | borderCardinalRule: boolean;
178 | xyCardinalRules: (
179 | | "rounded"
180 | | "gap"
181 | | "inset"
182 | | "scale"
183 | | "overflow"
184 | | "overscroll"
185 | )[];
186 | trblCardinalRules: ("p" | "m" | "scroll-m" | "scroll-p")[];
187 | objectFitPositionUniqueRules: ("fit" | "position")[];
188 | arbitraryRule: boolean;
189 | };
190 |
191 | export const EMPTY_GENERATION_STATE: GenerationState = {
192 | imports: [],
193 | constants: [],
194 | topConflictRule: [],
195 | flexDirectionWrapUniqueRules: [],
196 | flexBasisGrowShrinkConflictRule: [],
197 | textAlignSizeUniqueRules: [],
198 | bgUniqueRules: [],
199 | scrollUniqueRules: [],
200 | topUniqueRules: [],
201 | alignContentUniqueRule: false,
202 | listStylePositionUniqueRule: false,
203 | textDecorationStyleUniqueRule: false,
204 | borderStyleUniqueRule: false,
205 | divideStyleUniqueRule: false,
206 | outlineStyleUniqueRule: false,
207 | shadowUniqueRule: false,
208 | fontWeightUniqueRule: false,
209 | topSimpleRule: [],
210 | topSimpleRuleByType: [],
211 | borderCardinalRule: false,
212 | xyCardinalRules: [],
213 | trblCardinalRules: [],
214 | objectFitPositionUniqueRules: [],
215 | arbitraryRule: false,
216 | };
217 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/index.ts:
--------------------------------------------------------------------------------
1 | export { generateTailwindRuleSet } from "./gen";
2 | export type { TailwindRules, GenerateTailwindRuleSetOptions } from "./types";
3 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/process-utility.ts:
--------------------------------------------------------------------------------
1 | import { GenerationState } from "./generation-state";
2 | import { ResolvedRules } from "./types";
3 |
4 | export function processUtility(
5 | state: GenerationState,
6 | category: keyof ResolvedRules,
7 | _utility: U
8 | ) {
9 | switch (category) {
10 | case "other": {
11 | const utility = _utility as ResolvedRules["other"][number];
12 |
13 | switch (utility) {
14 | case "arbitrary":
15 | state.arbitraryRule = true;
16 | break;
17 | }
18 | break;
19 | }
20 |
21 | case "layout": {
22 | const utility = _utility as ResolvedRules["layout"][number];
23 |
24 | switch (utility) {
25 | case "aspectRatio":
26 | state.topSimpleRule.push("aspect");
27 | break;
28 |
29 | case "container":
30 | state.topSimpleRule.push("container");
31 | break;
32 |
33 | case "columns":
34 | state.topSimpleRule.push("columns");
35 | break;
36 |
37 | case "breakAfter":
38 | state.topSimpleRule.push("break-after");
39 | break;
40 |
41 | case "breakBefore":
42 | state.topSimpleRule.push("break-before");
43 | break;
44 |
45 | case "breakInside":
46 | state.topSimpleRule.push("break-inside");
47 | break;
48 |
49 | case "boxDecorationBreak":
50 | state.topSimpleRule.push("box-decoration");
51 | break;
52 |
53 | case "boxSizing":
54 | state.topSimpleRule.push("box");
55 | break;
56 |
57 | case "display":
58 | state.constants.push("DISPLAY");
59 | state.topUniqueRules.push("DISPLAY");
60 | break;
61 |
62 | case "floats":
63 | state.topSimpleRule.push("float");
64 | break;
65 |
66 | case "clear":
67 | state.topSimpleRule.push("clear");
68 | break;
69 |
70 | case "isolation":
71 | state.constants.push("ISOLATION");
72 | state.topUniqueRules.push("ISOLATION");
73 | break;
74 |
75 | case "objectFit":
76 | state.constants.push("OBJECT_FIT");
77 | state.objectFitPositionUniqueRules.push("fit");
78 | break;
79 |
80 | case "objectPosition":
81 | state.constants.push("BG_AND_OBJECT_POSITION");
82 | state.objectFitPositionUniqueRules.push("position");
83 | break;
84 |
85 | case "overflow":
86 | state.xyCardinalRules.push("overflow");
87 | break;
88 |
89 | case "overscrollBehavior":
90 | state.xyCardinalRules.push("overscroll");
91 | break;
92 |
93 | case "position":
94 | state.constants.push("POSITION");
95 | state.topUniqueRules.push("POSITION");
96 | break;
97 |
98 | case "topRightBottomLeft":
99 | state.topConflictRule.push("inset-x");
100 | state.topConflictRule.push("inset-y");
101 | state.topConflictRule.push("inset");
102 | state.xyCardinalRules.push("inset");
103 | state.topSimpleRule.push(
104 | "top",
105 | "left",
106 | "bottom",
107 | "right",
108 | "start",
109 | "end"
110 | );
111 | break;
112 |
113 | case "visibility":
114 | state.constants.push("VISIBILITY");
115 | state.topUniqueRules.push("VISIBILITY");
116 | break;
117 |
118 | case "zIndex":
119 | state.topSimpleRule.push("z");
120 | break;
121 | }
122 |
123 | break;
124 | }
125 |
126 | case "flexboxAndGrid": {
127 | const utility = _utility as ResolvedRules["flexboxAndGrid"][number];
128 |
129 | switch (utility) {
130 | case "flexBasis":
131 | state.topSimpleRule.push("basis");
132 | state.flexBasisGrowShrinkConflictRule.push("basis");
133 | break;
134 |
135 | case "flexDirection":
136 | state.constants.push("FLEX_DIRECTION");
137 | state.flexDirectionWrapUniqueRules.push("direction");
138 | break;
139 |
140 | case "flexWrap":
141 | state.constants.push("FLEX_WRAP");
142 | state.flexDirectionWrapUniqueRules.push("wrap");
143 | break;
144 |
145 | case "flex":
146 | state.topSimpleRule.push("flex");
147 | break;
148 |
149 | case "flexGrow":
150 | state.topSimpleRule.push("grow");
151 | state.flexBasisGrowShrinkConflictRule.push("grow");
152 | break;
153 |
154 | case "flexShrink":
155 | state.topSimpleRule.push("shrink");
156 | state.flexBasisGrowShrinkConflictRule.push("shrink");
157 | break;
158 |
159 | case "order":
160 | state.topSimpleRule.push("order");
161 | break;
162 |
163 | case "gridTemplateColumns":
164 | state.topSimpleRule.push("grid-cols");
165 | break;
166 |
167 | case "gridColumnStartEnd":
168 | state.topSimpleRule.push("col-start");
169 | state.topSimpleRule.push("col-end");
170 | state.topSimpleRule.push("col");
171 | break;
172 |
173 | case "gridTemplateRows":
174 | state.topSimpleRule.push("grid-rows");
175 | break;
176 |
177 | case "gridRowStartEnd":
178 | state.topSimpleRule.push("row-start");
179 | state.topSimpleRule.push("row-end");
180 | state.topSimpleRule.push("row");
181 | break;
182 |
183 | case "gridAutoFlow":
184 | state.topSimpleRule.push("grid-flow");
185 | break;
186 |
187 | case "gridAutoColumns":
188 | state.topSimpleRule.push("auto-cols");
189 | break;
190 |
191 | case "gridAutoRows":
192 | state.topSimpleRule.push("auto-rows");
193 | break;
194 |
195 | case "gap":
196 | state.xyCardinalRules.push("gap");
197 | break;
198 |
199 | case "justifyContent":
200 | state.topSimpleRule.push("justify");
201 | break;
202 |
203 | case "justifyItems":
204 | state.topSimpleRule.push("justify-items");
205 | break;
206 |
207 | case "justifySelf":
208 | state.topSimpleRule.push("justify-self");
209 | break;
210 |
211 | case "alignContent":
212 | state.constants.push("ALIGN_CONTENT");
213 | state.alignContentUniqueRule = true;
214 | break;
215 |
216 | case "alignItems":
217 | state.topSimpleRule.push("items");
218 | break;
219 |
220 | case "alignSelf":
221 | state.topSimpleRule.push("self");
222 | break;
223 |
224 | case "placeContent":
225 | state.topSimpleRule.push("place-content");
226 | break;
227 |
228 | case "placeItems":
229 | state.topSimpleRule.push("place-items");
230 | break;
231 |
232 | case "placeSelf":
233 | state.topSimpleRule.push("place-self");
234 | break;
235 | }
236 |
237 | break;
238 | }
239 |
240 | case "spacing": {
241 | const utility = _utility as ResolvedRules["spacing"][number];
242 |
243 | switch (utility) {
244 | case "padding":
245 | state.trblCardinalRules.push("p");
246 | break;
247 |
248 | case "margin":
249 | state.trblCardinalRules.push("m");
250 | break;
251 |
252 | case "spaceBetween":
253 | state.topSimpleRule.push("space-x-reverse");
254 | state.topSimpleRule.push("space-x");
255 | state.topSimpleRule.push("space-y-reverse");
256 | state.topSimpleRule.push("space-y");
257 | break;
258 | }
259 |
260 | break;
261 | }
262 |
263 | case "sizing": {
264 | const utility = _utility as ResolvedRules["sizing"][number];
265 |
266 | switch (utility) {
267 | default:
268 | // TODO
269 | }
270 |
271 | break;
272 | }
273 |
274 | case "typography": {
275 | const utility = _utility as ResolvedRules["typography"][number];
276 |
277 | switch (utility) {
278 | default:
279 | // TODO
280 | }
281 |
282 | break;
283 | }
284 |
285 | case "backgrounds": {
286 | const utility = _utility as ResolvedRules["backgrounds"][number];
287 |
288 | switch (utility) {
289 | default:
290 | // TODO
291 | }
292 |
293 | break;
294 | }
295 |
296 | case "borders": {
297 | const utility = _utility as ResolvedRules["borders"][number];
298 |
299 | switch (utility) {
300 | default:
301 | // TODO
302 | }
303 |
304 | break;
305 | }
306 |
307 | case "effects": {
308 | const utility = _utility as ResolvedRules["effects"][number];
309 |
310 | switch (utility) {
311 | default:
312 | // TODO
313 | }
314 |
315 | break;
316 | }
317 |
318 | case "filters": {
319 | const utility = _utility as ResolvedRules["filters"][number];
320 |
321 | switch (utility) {
322 | default:
323 | // TODO
324 | }
325 |
326 | break;
327 | }
328 |
329 | case "tables": {
330 | const utility = _utility as ResolvedRules["tables"][number];
331 |
332 | switch (utility) {
333 | default:
334 | // TODO
335 | }
336 |
337 | break;
338 | }
339 |
340 | case "transitionsAndAnimations": {
341 | const utility =
342 | _utility as ResolvedRules["transitionsAndAnimations"][number];
343 |
344 | switch (utility) {
345 | default:
346 | // TODO
347 | }
348 |
349 | break;
350 | }
351 |
352 | case "transforms": {
353 | const utility = _utility as ResolvedRules["transforms"][number];
354 |
355 | switch (utility) {
356 | default:
357 | // TODO
358 | }
359 |
360 | break;
361 | }
362 |
363 | case "interactivity": {
364 | const utility = _utility as ResolvedRules["interactivity"][number];
365 |
366 | switch (utility) {
367 | default:
368 | // TODO
369 | }
370 |
371 | break;
372 | }
373 |
374 | case "svg": {
375 | const utility = _utility as ResolvedRules["svg"][number];
376 |
377 | switch (utility) {
378 | default:
379 | // TODO
380 | }
381 |
382 | break;
383 | }
384 |
385 | case "accessibility": {
386 | const utility = _utility as ResolvedRules["accessibility"][number];
387 |
388 | switch (utility) {
389 | default:
390 | // TODO
391 | }
392 |
393 | break;
394 | }
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/types.ts:
--------------------------------------------------------------------------------
1 | import { A } from "ts-toolbelt";
2 |
3 | import { UTILITIES_BY_CATEGORY } from "./utilities-by-category";
4 |
5 | export type GenerateTailwindRuleSetOptions = {
6 | importPath?: string;
7 | target?: "file" | "console";
8 | exportName?: string;
9 | };
10 |
11 | export type CategoryRules =
12 | | boolean
13 | | ({ mode?: "whitelist" | "blacklist" } & { [K in T[number]]?: boolean });
14 |
15 | export type TailwindRules = A.Compute<
16 | {
17 | mode?: "whitelist" | "blacklist";
18 | } & {
19 | -readonly [K in keyof typeof UTILITIES_BY_CATEGORY]?: CategoryRules<
20 | (typeof UTILITIES_BY_CATEGORY)[K][number][]
21 | >;
22 | }
23 | >;
24 |
25 | export type ResolvedRules = A.Compute<{
26 | -readonly [K in keyof typeof UTILITIES_BY_CATEGORY]: (typeof UTILITIES_BY_CATEGORY)[K][number][];
27 | }>;
28 |
--------------------------------------------------------------------------------
/src/generate-tailwind-rule-set/utilities-by-category.ts:
--------------------------------------------------------------------------------
1 | export const UTILITIES_BY_CATEGORY = {
2 | other: ["arbitrary"],
3 | layout: [
4 | "aspectRatio",
5 | "container",
6 | "columns",
7 | "breakAfter",
8 | "breakBefore",
9 | "breakInside",
10 | "boxDecorationBreak",
11 | "boxSizing",
12 | "display",
13 | "floats",
14 | "clear",
15 | "isolation",
16 | "objectFit",
17 | "objectPosition",
18 | "overflow",
19 | "overscrollBehavior",
20 | "position",
21 | "topRightBottomLeft",
22 | "visibility",
23 | "zIndex",
24 | ],
25 | flexboxAndGrid: [
26 | "flexBasis",
27 | "flexDirection",
28 | "flexWrap",
29 | "flex",
30 | "flexGrow",
31 | "flexShrink",
32 | "order",
33 | "gridTemplateColumns",
34 | "gridColumnStartEnd",
35 | "gridTemplateRows",
36 | "gridRowStartEnd",
37 | "gridAutoFlow",
38 | "gridAutoColumns",
39 | "gridAutoRows",
40 | "gap",
41 | "justifyContent",
42 | "justifyItems",
43 | "justifySelf",
44 | "alignContent",
45 | "alignItems",
46 | "alignSelf",
47 | "placeContent",
48 | "placeItems",
49 | "placeSelf",
50 | ],
51 | spacing: ["padding", "margin", "spaceBetween"],
52 | sizing: ["width", "minWidth", "maxWidth", "height", "minHeight", "maxHeight"],
53 | typography: [
54 | "fontFamily",
55 | "fontSize",
56 | "fontSmoothing",
57 | "fontStyle",
58 | "fontWeight",
59 | "fontVariantNumeric",
60 | "letterSpacing",
61 | "lineClamp",
62 | "lineHeight",
63 | "listStyleImage",
64 | "listStylePosition",
65 | "listStyleType",
66 | "textAlign",
67 | "textColor",
68 | "textDecoration",
69 | "textDecorationColor",
70 | "textDecorationStyle",
71 | "textDecorationThickness",
72 | "textUnderlineOffset",
73 | "textTransform",
74 | "textOverflow",
75 | "textIndent",
76 | "verticalAlign",
77 | "whitespace",
78 | "wordBreak",
79 | "hyphens",
80 | "content",
81 | ],
82 | backgrounds: [
83 | "backgroundAttachment",
84 | "backgroundClip",
85 | "backgroundColor",
86 | "backgroundOrigin",
87 | "backgroundPosition",
88 | "backgroundRepeat",
89 | "backgroundSize",
90 | "backgroundImage",
91 | "gradientColorStops",
92 | ],
93 | borders: [
94 | "borderRadius",
95 | "borderWidth",
96 | "borderColor",
97 | "borderStyle",
98 | "divideWidth",
99 | "divideColor",
100 | "divideStyle",
101 | "outlineWidth",
102 | "outlineColor",
103 | "outlineStyle",
104 | "outlineOffset",
105 | "ringWidth",
106 | "ringColor",
107 | "ringOffsetWidth",
108 | "ringOffsetColor",
109 | ],
110 | effects: [
111 | "boxShadow",
112 | "boxShadowColor",
113 | "opacity",
114 | "mixBlendMode",
115 | "backgroundBlendMode",
116 | ],
117 | filters: [
118 | "blur",
119 | "brightness",
120 | "contrast",
121 | "dropShadow",
122 | "grayscale",
123 | "hueRotate",
124 | "invert",
125 | "saturate",
126 | "sepia",
127 | "backdropBlur",
128 | "backdropBrightness",
129 | "backdropContrast",
130 | "backdropGrayscale",
131 | "backdropHueRotate",
132 | "backdropInvert",
133 | "backdropOpacity",
134 | "backdropSaturate",
135 | "backdropSepia",
136 | ],
137 | tables: ["borderCollapse", "borderSpacing", "tableLayout", "captionSide"],
138 | transitionsAndAnimations: [
139 | "transitionProperty",
140 | "transitionDuration",
141 | "transitionTimingFunction",
142 | "transitionDelay",
143 | "animation",
144 | ],
145 | transforms: ["scale", "rotate", "translate", "skew", "transformOrigin"],
146 | interactivity: [
147 | "accentColor",
148 | "appearance",
149 | "cursor",
150 | "caretColor",
151 | "pointerEvents",
152 | "resize",
153 | "scrollBehavior",
154 | "scrollMargin",
155 | "scrollPadding",
156 | "scrollSnapAlign",
157 | "scrollSnapStop",
158 | "scrollSnapType",
159 | "touchAction",
160 | "userSelect",
161 | "willChange",
162 | ],
163 | svg: ["fill", "stroke", "strokeWidth"],
164 | accessibility: ["screenReaders"],
165 | } as const;
166 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createMerge } from "./lib/create-merge";
2 | import { tailwind } from "./tailwind";
3 |
4 | export { createMerge, tailwind };
5 | export {
6 | arbitraryRule,
7 | cardinalRule,
8 | cardinalRules,
9 | conflictRule,
10 | simpleRule,
11 | uniqueRule,
12 | } from "./rules";
13 |
14 | export const twMerge = createMerge(tailwind());
15 |
--------------------------------------------------------------------------------
/src/lib/create-lru-cache.ts:
--------------------------------------------------------------------------------
1 | interface LruCache {
2 | get(key: Key): Value | undefined;
3 | set(key: Key, value: Value): Value;
4 | }
5 |
6 | // LRU cache inspired from hashlru (https://github.com/dominictarr/hashlru/blob/v1.0.4/index.js) but object replaced with Map to improve performance
7 | export function createLruCache(
8 | maxCacheSize: number
9 | ): LruCache {
10 | if (maxCacheSize < 1)
11 | return { get: () => undefined, set: (_, value) => value };
12 |
13 | let cacheSize = 0;
14 | let cache = new Map();
15 | let previousCache = new Map();
16 |
17 | function update(key: Key, value: Value) {
18 | cache.set(key, value);
19 | cacheSize++;
20 |
21 | if (cacheSize > maxCacheSize) {
22 | cacheSize = 0;
23 | previousCache = cache;
24 | cache = new Map();
25 | }
26 | }
27 |
28 | return {
29 | get(key) {
30 | let value = cache.get(key);
31 | if (value !== undefined) return value;
32 | if ((value = previousCache.get(key)) !== undefined) {
33 | update(key, value);
34 | return value;
35 | }
36 | },
37 | set(key, value) {
38 | if (cache.has(key)) cache.set(key, value);
39 | else update(key, value);
40 | return value;
41 | },
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/lib/create-merge.ts:
--------------------------------------------------------------------------------
1 | import { Handler, RuleSet } from "../rules";
2 |
3 | import { createLruCache } from "./create-lru-cache";
4 | import { normalizeContext } from "./utils";
5 |
6 | type ParsedRule = [RegExp, Handler];
7 |
8 | export type CreateMergeConfig = {
9 | cacheSize?: number;
10 | separator?: string;
11 | prefix?: string;
12 | };
13 |
14 | export function createMerge(
15 | ruleSet: RuleSet,
16 | { cacheSize = 500, separator = ":", prefix }: CreateMergeConfig = {}
17 | ) {
18 | const cache = createLruCache(cacheSize);
19 |
20 | const parsedRuleSet = ruleSet.map(
21 | ([regExp, handler]) =>
22 | [
23 | new RegExp(
24 | `^(?.*${separator}!?|!?)?-?${prefix ? `${prefix}-` : ""}${regExp}`
25 | ),
26 | handler,
27 | ] as ParsedRule
28 | );
29 |
30 | function merge(className: string) {
31 | const cached = cache.get(className);
32 | if (cached !== undefined) return cached;
33 |
34 | const memoryStore: Partial>[] = [];
35 |
36 | const classes = className.split(" ");
37 |
38 | const outputClasses: string[] = [];
39 |
40 | // - for each class from right to left
41 | for (let classI = classes.length - 1; classI >= 0; classI--) {
42 | const currentClass = classes[classI]!;
43 | let didNotMatchOrWasContinued = true;
44 | // - for each rule
45 | for (let ruleI = 0; ruleI < parsedRuleSet.length; ruleI++) {
46 | const rule = parsedRuleSet[ruleI]!;
47 | const regexp = rule[0];
48 | const match = currentClass.match(regexp);
49 |
50 | // - if class matches rule, execute it
51 | if (match) {
52 | didNotMatchOrWasContinued = false;
53 | const groups = match.groups!;
54 | const context = normalizeContext(groups?.c ?? "", separator);
55 | const handler = rule[1];
56 |
57 | const memory = ((memoryStore[ruleI] ??= {})[context] ??= {});
58 |
59 | const result = handler(memory, groups!);
60 | const keepClass = result === true;
61 | const continueToNextRule = result === "c";
62 |
63 | if (keepClass) outputClasses.unshift(currentClass);
64 |
65 | // - finish with the class unless the rule says so
66 | if (!continueToNextRule) break;
67 |
68 | didNotMatchOrWasContinued = true;
69 | }
70 | }
71 |
72 | if (didNotMatchOrWasContinued) outputClasses.unshift(currentClass);
73 | }
74 |
75 | return cache.set(className, outputClasses.join(" "));
76 | }
77 |
78 | return merge;
79 | }
80 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | export function isNumericValue(value?: string) {
2 | if (!value) return true;
3 | const arbitraryValue = value.match(/^\[(.*)\]$/)?.[1];
4 | return !isNaN(parseInt(arbitraryValue ?? value));
5 | }
6 |
7 | function sortContextSection(section: string[], separator: string) {
8 | return section
9 | .sort((a, b) => {
10 | if (a.startsWith("[") || b.startsWith("[")) return 0;
11 | return a.localeCompare(b);
12 | })
13 | .join(separator);
14 | }
15 |
16 | export function normalizeContext(context: string, separator: string) {
17 | if (!context) return context;
18 | const important = context.endsWith("!");
19 | const variants = context.replace(/:!?$/, "").split(separator);
20 | let section: string[] = [];
21 | let normalizedSections: string[] = [];
22 | function commitSection() {
23 | if (section.length > 0)
24 | normalizedSections.push(sortContextSection(section, separator));
25 | }
26 | for (let i = 0; i < variants.length; i++) {
27 | const variant = variants[i]!;
28 | if (variant.startsWith("[")) {
29 | // is arbitrary variant
30 | commitSection();
31 | normalizedSections.push(variant);
32 | section = [];
33 | } else section.push(variant);
34 | }
35 | commitSection();
36 |
37 | return `${normalizedSections.join(separator)}${separator}${
38 | important ? "!" : ""
39 | }`;
40 | }
41 |
--------------------------------------------------------------------------------
/src/rules.ts:
--------------------------------------------------------------------------------
1 | import { isNumericValue } from "./lib/utils";
2 |
3 | export type Handler = (
4 | memory: T,
5 | matches: NonNullable
6 | ) => boolean | "c"; // keep class | continue to next rule
7 |
8 | export type Rule = [string, Handler];
9 | export type RuleSet = Rule[];
10 |
11 | export const TRAILING_SLASH_REGEXP = "(\\/\\d+)?";
12 | export const VALUE_REGEXP = `(-(?.+?)${TRAILING_SLASH_REGEXP})?`;
13 |
14 | // simple rule
15 | // -----------
16 |
17 | export type SimpleHandlerOptions = { byType?: boolean };
18 |
19 | export function createSimpleHandler({ byType }: SimpleHandlerOptions = {}) {
20 | const simpleHandler: Handler<
21 | Record>>
22 | > = (memory, { v: value, t: target }) => {
23 | const type = byType && isNumericValue(value) ? "number" : "other";
24 | const mem = (memory[target!] ??= {});
25 |
26 | // seen before
27 | if (mem[type]) return false;
28 |
29 | // never seen
30 | return (mem[type] = true);
31 | };
32 |
33 | return simpleHandler;
34 | }
35 |
36 | export type SimpleRuleOptions = SimpleHandlerOptions;
37 |
38 | export function simpleRule(
39 | target: string,
40 | { byType }: SimpleRuleOptions = {}
41 | ): Rule {
42 | const regExp = `(?${target})${VALUE_REGEXP}$`;
43 | return [regExp, createSimpleHandler({ byType })];
44 | }
45 |
46 | // cardinal rule
47 | // -------------
48 |
49 | export type CardinalHandlerOptions = {
50 | byType?: boolean;
51 | };
52 |
53 | type Direction = string;
54 |
55 | const CARDINAL_OVERRIDES: Record = {
56 | t: ",y,tl,tr",
57 | r: ",x,tr,br",
58 | b: ",y,br,bl",
59 | l: ",x,bl,tl",
60 | x: "",
61 | y: "",
62 | s: "",
63 | e: "",
64 | ss: ",e,s",
65 | se: ",e,s",
66 | es: ",e,s",
67 | ee: ",e,s",
68 | };
69 | const CARDINAL_DIRECTIONS =
70 | Object.keys(CARDINAL_OVERRIDES).join("|") + "|tl|tr|br|bl";
71 |
72 | export function createCardinalHandler({ byType }: CardinalHandlerOptions = {}) {
73 | const cardinalHandler: Handler<
74 | Partial>>> & {
75 | _?: Partial>>;
76 | }
77 | > = (memory, { v: value, d: direction = "" }) => {
78 | const type = byType && isNumericValue(value) ? "number" : "other";
79 | const mem = (memory[direction] ??= {});
80 |
81 | // seen before
82 | if (mem[type]) return false;
83 |
84 | // apply override
85 | const memOverriders = ((memory._ ??= {})[type] ??= new Set());
86 | if (
87 | CARDINAL_OVERRIDES[direction]
88 | ?.split(",")
89 | .some((dir) => memOverriders.has(dir))
90 | )
91 | return false;
92 |
93 | // remember overrider
94 | memOverriders.add(direction);
95 |
96 | // never seen
97 | mem[type] = true;
98 | return true;
99 | };
100 |
101 | return cardinalHandler;
102 | }
103 |
104 | export type CardinalRuleOptions = {
105 | /**
106 | * Whether the direction is dash-separated (e.g. `border-t-2`)
107 | * @default true
108 | */
109 | dash?: boolean;
110 | } & CardinalHandlerOptions;
111 |
112 | export function cardinalRule(
113 | target: string,
114 | { dash = true, byType }: CardinalRuleOptions = {}
115 | ): Rule {
116 | const _target = `${target}(${dash ? "-" : ""}(?${CARDINAL_DIRECTIONS}))?`;
117 | const regExp = `${_target}${VALUE_REGEXP}$`;
118 | return [regExp, createCardinalHandler({ byType })];
119 | }
120 |
121 | export function cardinalRules(targets: string, options?: CardinalRuleOptions) {
122 | const _targets = targets.split("|");
123 | return _targets.map((target) => cardinalRule(target, options));
124 | }
125 |
126 | // unique rule
127 | // -----------
128 |
129 | export function createUniqueHandler() {
130 | const uniqueHandler: Handler> = (memory, groups) => {
131 | const key = Object.entries(groups).find((x) => x[1])![0];
132 | return memory[key] ? false : (memory[key] = true);
133 | };
134 | return uniqueHandler;
135 | }
136 |
137 | export type UniqueRuleOptions = { prefix?: string; def?: boolean };
138 |
139 | export function uniqueRule(targets: (string | string[])[]): Rule {
140 | const regExp = `(${targets
141 | .map((target, targetI) =>
142 | Array.isArray(target)
143 | ? target
144 | .slice(1)
145 | .map(
146 | (subtarget, subtargetI) =>
147 | `(?${`${target[0]}-(${subtarget})`})`
148 | )
149 | : `(?${target})`
150 | )
151 | .flat()
152 | .join("|")})${TRAILING_SLASH_REGEXP}$`;
153 | return [regExp, createUniqueHandler()];
154 | }
155 |
156 | // arbitrary rule
157 | // --------------
158 |
159 | export function createArbitraryHandler() {
160 | const arbitraryHandler: Handler> = (
161 | memory,
162 | { p: property }
163 | ) => {
164 | const mem = (memory[property!] ??= {});
165 |
166 | // seen before
167 | if (mem.done) return false;
168 |
169 | // never seen
170 | return (mem.done = true);
171 | };
172 |
173 | return arbitraryHandler;
174 | }
175 |
176 | export function arbitraryRule(): Rule {
177 | return [`\\[(?.+?):.*\\]$`, createArbitraryHandler()];
178 | }
179 |
180 | // conflict rule
181 | // -------------
182 |
183 | export type ConflictRuleTargets = Record;
184 |
185 | export function createConflictHandler(targets: ConflictRuleTargets) {
186 | const overridableMap: Record = {};
187 | Object.entries(targets).forEach(([overridingUtility, overridableUtilities]) =>
188 | overridableUtilities.split("|").forEach((value) => {
189 | overridableMap[value] ??= [];
190 | overridableMap[value]!.push(overridingUtility);
191 | })
192 | );
193 |
194 | const conflictHandler: Handler> = (
195 | memory,
196 | { u: utility }
197 | ) => {
198 | // is overridable utility and overriding utility has been seen
199 | const skipClass = Boolean(
200 | utility! in overridableMap &&
201 | overridableMap[utility!]!.some((u) => memory[u])
202 | );
203 | if (skipClass) return false;
204 |
205 | // is overriding utility
206 | if (utility! in targets) memory[utility!] = true;
207 |
208 | // continue evaluating other rules
209 | return "c";
210 | };
211 |
212 | return conflictHandler;
213 | }
214 |
215 | export function conflictRule(targets: ConflictRuleTargets): Rule {
216 | const overridingUtilities = Object.keys(targets);
217 | const overridableUtilities = Object.values(targets).join("|").split("|");
218 | const matchingClasses = [...overridingUtilities, ...overridableUtilities];
219 | const utility = `(?${matchingClasses.join("|")})`;
220 | const regExp = `${utility}${VALUE_REGEXP}$`;
221 | return [regExp, createConflictHandler(targets)];
222 | }
223 |
--------------------------------------------------------------------------------
/src/tailwind.ts:
--------------------------------------------------------------------------------
1 | import {
2 | RuleSet,
3 | uniqueRule,
4 | simpleRule,
5 | cardinalRules,
6 | cardinalRule,
7 | arbitraryRule,
8 | conflictRule,
9 | } from "./rules";
10 |
11 | const DISPLAY =
12 | "block|inline-block|inline-flex|inline-table|inline-grid|inline|flex|table-caption|table-cell|table-column-group|table-column|table-footer-group|table-header-group|table-row-group|table-row|table|flow-root|grid|contents|list-item|hidden";
13 |
14 | const ISOLATION = "isolate|isolation-auto";
15 |
16 | const OBJECT_FIT = "contain|cover|fill|none|scale-down";
17 | const BG_AND_OBJECT_POSITION =
18 | "bottom|center|left|left-bottom|left-top|right|right-bottom|right-top|top";
19 |
20 | const POSITION = "static|fixed|absolute|relative|sticky";
21 |
22 | const VISIBILITY = "visible|invisible|collapse";
23 |
24 | const FLEX_DIRECTION = "row|row-reverse|col|col-reverse";
25 | const FLEX_WRAP = "wrap|wrap-reverse|nowrap";
26 |
27 | const ALIGN_CONTENT =
28 | "normal|center|start|end|between|around|evenly|baseline|stretch";
29 |
30 | const FONT_AND_SHADOW_SIZE = "xs|sm|base|md|lg|xl|[\\d.]+xl|inner|none";
31 | const FONT_SMOOTHING = "antialiased|subpixel-antialiased";
32 | const FONT_STYLE = "italic|not-italic";
33 | const FONT_WEIGHT =
34 | "thin|extralight|light|normal|medium|semibold|bold|extrabold|black";
35 |
36 | const LIST_STYLE_POSITION = "inside|outside";
37 |
38 | const TEXT_ALIGN = "left|center|right|justify|start|end";
39 | const TEXT_DECORATION = "underline|overline|line-through|no-underline";
40 | const TEXT_DECORATION_STYLE = "solid|double|dotted|dashed|wavy";
41 | const TEXT_TRANSFORM = "uppercase|lowercase|capitalize|normal-case";
42 | const TEXT_OVERFLOW = "truncate|text-ellipsis|text-clip";
43 |
44 | const BG_ATTACHMENT = "fixed|local|scroll";
45 | const BG_REPEAT =
46 | "repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space";
47 | const BG_SIZE = "auto|cover|contain";
48 |
49 | const BORDER_AND_OUTLINE_STYLE = "solid|dashed|dotted|double|hidden|none";
50 |
51 | const FVN_FIGURE = "lining-nums|oldstyle-nums";
52 | const FVN_SPACING = "proportional-nums|tabular-nums";
53 | const FVN_FRACTION = "diagonal-fractions|stacked-fractions";
54 |
55 | const SCROLL_BEHAVIOR = "auto|smooth";
56 | const SCROLL_SNAP_ALIGN = "start|end|center|none";
57 | const SCROLL_SNAP_STOP = "normal|always";
58 | const SCROLL_SNAP_TYPE = "none|x|y|both|mandatory|proximity";
59 |
60 | // TODO: text-/20 should override line-height (leading)
61 | // TODO: ^ same with opacities and other trailing slash values
62 | // TODO: text-decoration-thickness (conflicts with text-decoration-color and there are custom values: auto and from-font)
63 | export function tailwind(): RuleSet {
64 | return [
65 | // these rules are at the top because they need to run before others
66 | conflictRule({
67 | "inset-x": "left|right",
68 | "inset-y": "top|bottom",
69 | inset: "inset-x|inset-y|start|end|left|right|top|bottom",
70 | "sr-only": "not-sr-only",
71 | "not-sr-only": "sr-only",
72 | "normal-nums":
73 | "ordinal|slashed-zero|lining-nums|oldstyle-nums|proportional-nums|tabular-nums|diagonal-fractions|stacked-fractons",
74 | ordinal: "normal-nums",
75 | "slashed-zero": "normal-nums",
76 | "lining-nums": "normal-nums",
77 | "oldstyle-nums": "normal-nums",
78 | "proportional-nums": "normal-nums",
79 | "tabular-nums": "normal-nums",
80 | "diagonal-fractions": "normal-nums",
81 | "stacked-fractons": "normal-nums",
82 | "bg-gradient": "bg-none",
83 | "bg-none": "bg-gradient",
84 | }),
85 | uniqueRule([
86 | DISPLAY,
87 | ISOLATION,
88 | POSITION,
89 | VISIBILITY,
90 | FONT_SMOOTHING,
91 | FONT_STYLE,
92 | FVN_FIGURE,
93 | FVN_SPACING,
94 | FVN_FRACTION,
95 | TEXT_DECORATION,
96 | TEXT_TRANSFORM,
97 | TEXT_OVERFLOW,
98 | ]),
99 | uniqueRule([
100 | ["content", ALIGN_CONTENT],
101 | ["list", LIST_STYLE_POSITION],
102 | ["decoration", TEXT_DECORATION_STYLE],
103 | ["border", BORDER_AND_OUTLINE_STYLE],
104 | ["divide", BORDER_AND_OUTLINE_STYLE],
105 | ["outline|outline", BORDER_AND_OUTLINE_STYLE],
106 | ["shadow", FONT_AND_SHADOW_SIZE],
107 | ["font", FONT_WEIGHT],
108 | ["object", OBJECT_FIT, BG_AND_OBJECT_POSITION],
109 | ]),
110 | uniqueRule([
111 | [
112 | "scroll",
113 | SCROLL_BEHAVIOR,
114 | SCROLL_SNAP_ALIGN,
115 | SCROLL_SNAP_STOP,
116 | SCROLL_SNAP_TYPE,
117 | ],
118 | ["bg", BG_ATTACHMENT, BG_AND_OBJECT_POSITION, BG_REPEAT, BG_SIZE],
119 | ["text", TEXT_ALIGN, FONT_AND_SHADOW_SIZE],
120 | ["flex", FLEX_DIRECTION, FLEX_WRAP],
121 | ]),
122 | conflictRule({ flex: "basis|grow|shrink" }),
123 | // -----------------------------------------------------------------
124 | simpleRule(
125 | "accent|align|animate|aspect|auto-cols|auto-rows|backdrop-blur|backdrop-brightness|backdrop-contrast|backdrop-grayscale|backdrop-hue-rotate|backdrop-invert|backdrop-opacity|backdrop-saturate|backdrop-sepia|basis|bg-blend|bg-clip|bg-origin|bg-none|bg-gradient|bg|blur|border-collapse|border-spacing|bottom|box-decoration|box|break-after|break-before|break-inside|break|brightness|caption|caret|clear|col-end|col-start|columns|col|content|contrast|cursor|decoration|delay|divide-x-reverse|divide-x|divide-y-reverse|divide-y|divide|drop-shadow|duration|ease|end|fill|flex|float|grayscale|grid-cols|grid-flow|grid-rows|grow|hue-rotate|hyphens|h|indent|invert|items|justify-items|justify-self|justify|leading|left|line-clamp|list-image|list|max-h|max-w|min-h|min-w|mix-blend|opacity|order|origin|outline-offset|place-content|place-items|place-self|pointer-events|resize|right|ring-inset|rotate|row-end|row-start|row|saturate|select|self|sepia|shadow|shrink|skew-x|skew-y|space-x-reverse|space-x|space-y-reverse|space-y|start|table|top|touch|tracking|transition|translate-x|translate-y|underline-offset|whitespace|will-change|w|z"
126 | ),
127 | simpleRule("text|outline|ring-offset|ring|from|via|to|stroke|font", {
128 | byType: true,
129 | }),
130 | cardinalRule("border", { byType: true }),
131 | ...cardinalRules("rounded|gap|inset|scale|overflow|overscroll"),
132 | ...cardinalRules("p|m|scroll-m|scroll-p", { dash: false }),
133 | arbitraryRule(),
134 | ];
135 | }
136 |
--------------------------------------------------------------------------------
/tests/arbitrary-properties.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("handles arbitrary property conflicts correctly", () => {
4 | expect(twMerge("[paint-order:markers] [paint-order:normal]")).toBe(
5 | "[paint-order:normal]"
6 | );
7 | expect(
8 | twMerge(
9 | "[paint-order:markers] [--my-var:2rem] [paint-order:normal] [--my-var:4px]"
10 | )
11 | ).toBe("[paint-order:normal] [--my-var:4px]");
12 | });
13 |
14 | test("handles arbitrary property conflicts with modifiers correctly", () => {
15 | expect(twMerge("[paint-order:markers] hover:[paint-order:normal]")).toBe(
16 | "[paint-order:markers] hover:[paint-order:normal]"
17 | );
18 | expect(
19 | twMerge("hover:[paint-order:markers] hover:[paint-order:normal]")
20 | ).toBe("hover:[paint-order:normal]");
21 | expect(
22 | twMerge(
23 | "hover:focus:[paint-order:markers] focus:hover:[paint-order:normal]"
24 | )
25 | ).toBe("focus:hover:[paint-order:normal]");
26 | expect(
27 | twMerge(
28 | "[paint-order:markers] [paint-order:normal] [--my-var:2rem] lg:[--my-var:4px]"
29 | )
30 | ).toBe("[paint-order:normal] [--my-var:2rem] lg:[--my-var:4px]");
31 | });
32 |
33 | test("handles complex arbitrary property conflicts correctly", () => {
34 | expect(
35 | twMerge("[-unknown-prop:::123:::] [-unknown-prop:url(https://hi.com)]")
36 | ).toBe("[-unknown-prop:url(https://hi.com)]");
37 | });
38 |
39 | test("handles important modifier correctly", () => {
40 | expect(twMerge("![some:prop] [some:other]")).toBe(
41 | "![some:prop] [some:other]"
42 | );
43 | expect(twMerge("![some:prop] [some:other] [some:one] ![some:another]")).toBe(
44 | "[some:one] ![some:another]"
45 | );
46 | });
47 |
--------------------------------------------------------------------------------
/tests/arbitrary-values.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("handles simple conflicts with arbitrary values correctly", () => {
4 | expect(twMerge("m-[2px] m-[10px]")).toBe("m-[10px]");
5 | expect(
6 | twMerge(
7 | "m-[2px] m-[11svmin] m-[12in] m-[13lvi] m-[14vb] m-[15vmax] m-[16mm] m-[17%] m-[18em] m-[19px] m-[10dvh]"
8 | )
9 | ).toBe("m-[10dvh]");
10 | expect(
11 | twMerge(
12 | "h-[10px] h-[11cqw] h-[12cqh] h-[13cqi] h-[14cqb] h-[15cqmin] h-[16cqmax]"
13 | )
14 | ).toBe("h-[16cqmax]");
15 | expect(twMerge("z-20 z-[99]")).toBe("z-[99]");
16 | expect(twMerge("my-[2px] m-[10rem]")).toBe("m-[10rem]");
17 | expect(twMerge("cursor-pointer cursor-[grab]")).toBe("cursor-[grab]");
18 | expect(twMerge("m-[2px] m-[calc(100%-var(--arbitrary))]")).toBe(
19 | "m-[calc(100%-var(--arbitrary))]"
20 | );
21 | expect(twMerge("m-[2px] m-[length:var(--mystery-var)]")).toBe(
22 | "m-[length:var(--mystery-var)]"
23 | );
24 | expect(twMerge("opacity-10 opacity-[0.025]")).toBe("opacity-[0.025]");
25 | expect(twMerge("scale-75 scale-[1.7]")).toBe("scale-[1.7]");
26 | expect(twMerge("brightness-90 brightness-[1.75]")).toBe("brightness-[1.75]");
27 | });
28 |
29 | test("handles arbitrary length conflicts with labels and modifiers correctly", () => {
30 | expect(twMerge("hover:m-[2px] hover:m-[length:var(--c)]")).toBe(
31 | "hover:m-[length:var(--c)]"
32 | );
33 | expect(twMerge("hover:focus:m-[2px] focus:hover:m-[length:var(--c)]")).toBe(
34 | "focus:hover:m-[length:var(--c)]"
35 | );
36 | expect(
37 | twMerge("border-b border-[color:rgb(var(--color-gray-500-rgb)/50%))]")
38 | ).toBe("border-b border-[color:rgb(var(--color-gray-500-rgb)/50%))]");
39 | expect(
40 | twMerge("border-[color:rgb(var(--color-gray-500-rgb)/50%))] border-b")
41 | ).toBe("border-[color:rgb(var(--color-gray-500-rgb)/50%))] border-b");
42 | expect(
43 | twMerge(
44 | "border-b border-[color:rgb(var(--color-gray-500-rgb)/50%))] border-some-coloooor"
45 | )
46 | ).toBe("border-b border-some-coloooor");
47 | });
48 |
49 | test("handles complex arbitrary value conflicts correctly", () => {
50 | expect(twMerge("grid-rows-[1fr,auto] grid-rows-2")).toBe("grid-rows-2");
51 | expect(twMerge("grid-rows-[repeat(20,minmax(0,1fr))] grid-rows-3")).toBe(
52 | "grid-rows-3"
53 | );
54 | });
55 |
--------------------------------------------------------------------------------
/tests/arbitrary-variants.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("basic arbitrary variants", () => {
4 | expect(twMerge("[&>*]:underline [&>*]:line-through")).toBe(
5 | "[&>*]:line-through"
6 | );
7 | expect(
8 | twMerge("[&>*]:underline [&>*]:line-through [&_div]:line-through")
9 | ).toBe("[&>*]:line-through [&_div]:line-through");
10 | expect(
11 | twMerge("supports-[display:grid]:flex supports-[display:grid]:grid")
12 | ).toBe("supports-[display:grid]:grid");
13 | });
14 |
15 | test("arbitrary variants with modifiers", () => {
16 | expect(
17 | twMerge("dark:lg:hover:[&>*]:underline dark:lg:hover:[&>*]:line-through")
18 | ).toBe("dark:lg:hover:[&>*]:line-through");
19 | expect(
20 | twMerge("dark:lg:hover:[&>*]:underline dark:hover:lg:[&>*]:line-through")
21 | ).toBe("dark:hover:lg:[&>*]:line-through");
22 | // Whether a modifier is before or after arbitrary variant matters
23 | expect(twMerge("hover:[&>*]:underline [&>*]:hover:line-through")).toBe(
24 | "hover:[&>*]:underline [&>*]:hover:line-through"
25 | );
26 | expect(
27 | twMerge(
28 | "hover:dark:[&>*]:underline dark:hover:[&>*]:underline dark:[&>*]:hover:line-through"
29 | )
30 | ).toBe("dark:hover:[&>*]:underline dark:[&>*]:hover:line-through");
31 | });
32 |
33 | test("arbitrary variants with complex syntax in them", () => {
34 | expect(
35 | twMerge(
36 | "[@media_screen{@media(hover:hover)}]:underline [@media_screen{@media(hover:hover)}]:line-through"
37 | )
38 | ).toBe("[@media_screen{@media(hover:hover)}]:line-through");
39 | expect(
40 | twMerge(
41 | "hover:[@media_screen{@media(hover:hover)}]:underline hover:[@media_screen{@media(hover:hover)}]:line-through"
42 | )
43 | ).toBe("hover:[@media_screen{@media(hover:hover)}]:line-through");
44 | });
45 |
46 | test("arbitrary variants with attribute selectors", () => {
47 | expect(twMerge("[&[data-open]]:underline [&[data-open]]:line-through")).toBe(
48 | "[&[data-open]]:line-through"
49 | );
50 | });
51 |
52 | test("arbitrary variants with multiple attribute selectors", () => {
53 | expect(
54 | twMerge(
55 | "[&[data-foo][data-bar]:not([data-baz])]:underline [&[data-foo][data-bar]:not([data-baz])]:line-through"
56 | )
57 | ).toBe("[&[data-foo][data-bar]:not([data-baz])]:line-through");
58 | });
59 |
60 | test("multiple arbitrary variants", () => {
61 | expect(twMerge("[&>*]:[&_div]:underline [&>*]:[&_div]:line-through")).toBe(
62 | "[&>*]:[&_div]:line-through"
63 | );
64 | expect(twMerge("[&>*]:[&_div]:underline [&_div]:[&>*]:line-through")).toBe(
65 | "[&>*]:[&_div]:underline [&_div]:[&>*]:line-through"
66 | );
67 | expect(
68 | twMerge(
69 | "hover:dark:[&>*]:focus:disabled:[&_div]:underline dark:hover:[&>*]:disabled:focus:[&_div]:line-through"
70 | )
71 | ).toBe("dark:hover:[&>*]:disabled:focus:[&_div]:line-through");
72 | expect(
73 | twMerge(
74 | "hover:dark:[&>*]:focus:[&_div]:disabled:underline dark:hover:[&>*]:disabled:focus:[&_div]:line-through"
75 | )
76 | ).toBe(
77 | "hover:dark:[&>*]:focus:[&_div]:disabled:underline dark:hover:[&>*]:disabled:focus:[&_div]:line-through"
78 | );
79 | });
80 |
81 | test("arbitrary variants with arbitrary properties", () => {
82 | expect(twMerge("[&>*]:[color:red] [&>*]:[color:blue]")).toBe(
83 | "[&>*]:[color:blue]"
84 | );
85 | expect(
86 | twMerge(
87 | "[&[data-foo][data-bar]:not([data-baz])]:nod:noa:[color:red] [&[data-foo][data-bar]:not([data-baz])]:noa:nod:[color:blue]"
88 | )
89 | ).toBe("[&[data-foo][data-bar]:not([data-baz])]:noa:nod:[color:blue]");
90 | });
91 |
--------------------------------------------------------------------------------
/tests/class-group-conflicts.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("merges classes from same group correctly", () => {
4 | expect(twMerge("overflow-x-auto overflow-x-hidden")).toBe(
5 | "overflow-x-hidden"
6 | );
7 | expect(twMerge("w-full w-fit")).toBe("w-fit");
8 | expect(twMerge("overflow-x-auto overflow-x-hidden overflow-x-scroll")).toBe(
9 | "overflow-x-scroll"
10 | );
11 | expect(
12 | twMerge("overflow-x-auto hover:overflow-x-hidden overflow-x-scroll")
13 | ).toBe("hover:overflow-x-hidden overflow-x-scroll");
14 | expect(
15 | twMerge(
16 | "overflow-x-auto hover:overflow-x-hidden hover:overflow-x-auto overflow-x-scroll"
17 | )
18 | ).toBe("hover:overflow-x-auto overflow-x-scroll");
19 | });
20 |
21 | test("merges classes from Font Variant Numeric section correctly", () => {
22 | expect(twMerge("lining-nums tabular-nums diagonal-fractions")).toBe(
23 | "lining-nums tabular-nums diagonal-fractions"
24 | );
25 | expect(twMerge("normal-nums tabular-nums diagonal-fractions")).toBe(
26 | "tabular-nums diagonal-fractions"
27 | );
28 | expect(twMerge("tabular-nums diagonal-fractions normal-nums")).toBe(
29 | "normal-nums"
30 | );
31 | expect(twMerge("tabular-nums proportional-nums")).toBe("proportional-nums");
32 | });
33 |
--------------------------------------------------------------------------------
/tests/colors.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("handles color conflicts properly", () => {
4 | expect(twMerge("bg-grey-5 bg-hotpink")).toBe("bg-hotpink");
5 | expect(twMerge("hover:bg-grey-5 hover:bg-hotpink")).toBe("hover:bg-hotpink");
6 | });
7 |
--------------------------------------------------------------------------------
/tests/conflicts-across-class-groups.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("handles conflicts across class groups correctly", () => {
4 | expect(twMerge("inset-1 inset-x-1")).toBe("inset-1 inset-x-1");
5 | expect(twMerge("inset-x-1 inset-1")).toBe("inset-1");
6 | expect(twMerge("inset-x-1 left-1 inset-1")).toBe("inset-1");
7 | expect(twMerge("inset-x-1 inset-1 left-1")).toBe("inset-1 left-1");
8 | expect(twMerge("inset-x-1 right-1 inset-1")).toBe("inset-1");
9 | expect(twMerge("inset-x-1 right-1 inset-x-1")).toBe("inset-x-1");
10 | expect(twMerge("inset-x-1 right-1 inset-y-1")).toBe(
11 | "inset-x-1 right-1 inset-y-1"
12 | );
13 | expect(twMerge("right-1 inset-x-1 inset-y-1")).toBe("inset-x-1 inset-y-1");
14 | expect(twMerge("inset-x-1 hover:left-1 inset-1")).toBe(
15 | "hover:left-1 inset-1"
16 | );
17 | });
18 |
19 | test("ring and shadow classes do not create conflict", () => {
20 | expect(twMerge("ring shadow")).toBe("ring shadow");
21 | expect(twMerge("ring-2 shadow-md")).toBe("ring-2 shadow-md");
22 | expect(twMerge("shadow ring")).toBe("shadow ring");
23 | expect(twMerge("shadow-md ring-2")).toBe("shadow-md ring-2");
24 | });
25 |
--------------------------------------------------------------------------------
/tests/content-utilities.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("merges content utilities correctly", () => {
4 | expect(twMerge("content-['hello'] content-[attr(data-content)]")).toBe(
5 | "content-[attr(data-content)]"
6 | );
7 | });
8 |
--------------------------------------------------------------------------------
/tests/docs-examples.test.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 |
3 | import globby from "globby";
4 |
5 | import { twMerge } from "../src";
6 |
7 | const twMergeExampleRegex =
8 | /twMerge\((?[\w\s\-:[\]#(),!&\n'"]+?)\)(?!.*(?.+)['"]/g;
9 |
10 | test("docs examples", () => {
11 | expect.assertions(23);
12 |
13 | return forEachFile(["README.md", "docs/**/*.md"], (fileContent) => {
14 | Array.from(fileContent.matchAll(twMergeExampleRegex)).forEach((match) => {
15 | // eslint-disable-next-line no-eval
16 | const args = eval(`[${match.groups!.arguments}]`);
17 | // @ts-ignore
18 | expect(twMerge(...args)).toBe(match.groups!.result);
19 | });
20 | });
21 | });
22 |
23 | async function forEachFile(
24 | patterns: string | string[],
25 | callback: (fileContent: string) => void
26 | ) {
27 | const paths = await globby(patterns, {
28 | dot: true,
29 | absolute: true,
30 | onlyFiles: true,
31 | unique: true,
32 | });
33 |
34 | await Promise.all(
35 | paths.map((filePath) =>
36 | fs.promises.readFile(filePath, { encoding: "utf-8" }).then(callback)
37 | )
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/tests/important-modifier.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("merges tailwind classes with important modifier correctly", () => {
4 | expect(twMerge("!font-medium !font-bold")).toBe("!font-bold");
5 | expect(twMerge("!font-medium !font-bold font-thin")).toBe(
6 | "!font-bold font-thin"
7 | );
8 | expect(twMerge("!right-2 !-inset-x-px")).toBe("!-inset-x-px");
9 | expect(twMerge("focus:!inline focus:!block")).toBe("focus:!block");
10 | });
11 |
--------------------------------------------------------------------------------
/tests/negative-values.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("handles negative value conflicts correctly", () => {
4 | expect(twMerge("-m-2 -m-5")).toBe("-m-5");
5 | expect(twMerge("-top-12 -top-2000")).toBe("-top-2000");
6 | });
7 |
8 | test("handles conflicts between positive and negative values correctly", () => {
9 | expect(twMerge("-m-2 m-auto")).toBe("m-auto");
10 | expect(twMerge("top-12 -top-69")).toBe("-top-69");
11 | });
12 |
13 | test("handles conflicts across groups with negative values correctly", () => {
14 | expect(twMerge("-right-1 inset-x-1")).toBe("inset-x-1");
15 | expect(twMerge("hover:focus:-right-1 focus:hover:inset-x-1")).toBe(
16 | "focus:hover:inset-x-1"
17 | );
18 | });
19 |
--------------------------------------------------------------------------------
/tests/non-conflicting-classes.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("merges non-conflicting classes correctly", () => {
4 | expect(twMerge("border-t border-white/10")).toBe("border-t border-white/10");
5 | expect(twMerge("border-t border-white")).toBe("border-t border-white");
6 | expect(twMerge("text-2xl text-3.5xl text-black")).toBe(
7 | "text-3.5xl text-black"
8 | );
9 | });
10 |
--------------------------------------------------------------------------------
/tests/non-tailwind-classes.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("does not alter non-tailwind classes", () => {
4 | expect(twMerge("non-tailwind-class inline block")).toBe(
5 | "non-tailwind-class block"
6 | );
7 | expect(twMerge("inline block inline-1")).toBe("block inline-1");
8 | expect(twMerge("inline block i-inline")).toBe("block i-inline");
9 | expect(twMerge("focus:inline focus:block focus:inline-1")).toBe(
10 | "focus:block focus:inline-1"
11 | );
12 | });
13 |
--------------------------------------------------------------------------------
/tests/per-side-border-colors.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("merges classes with per-side border colors correctly", () => {
4 | expect(twMerge("border-t-some-blue border-t-other-blue")).toBe(
5 | "border-t-other-blue"
6 | );
7 | expect(twMerge("border-t-some-blue border-some-blue")).toBe(
8 | "border-some-blue"
9 | );
10 | });
11 |
--------------------------------------------------------------------------------
/tests/prefixes.test.ts:
--------------------------------------------------------------------------------
1 | import { createMerge, tailwind } from "../src";
2 |
3 | test("prefix working correctly", () => {
4 | const twMerge = createMerge(tailwind(), { prefix: "tw" });
5 |
6 | expect(twMerge("tw-block tw-hidden")).toBe("tw-hidden");
7 | expect(twMerge("block hidden")).toBe("block hidden");
8 |
9 | expect(twMerge("tw-p-3 tw-p-2")).toBe("tw-p-2");
10 | expect(twMerge("p-3 p-2")).toBe("p-3 p-2");
11 |
12 | expect(twMerge("!tw-right-0 !tw-inset-0")).toBe("!tw-inset-0");
13 |
14 | expect(twMerge("hover:focus:!tw-right-0 focus:hover:!tw-inset-0")).toBe(
15 | "focus:hover:!tw-inset-0"
16 | );
17 | });
18 |
--------------------------------------------------------------------------------
/tests/pseudo-variants.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("handles pseudo variants conflicts properly", () => {
4 | expect(twMerge("empty:p-2 empty:p-3")).toBe("empty:p-3");
5 | expect(twMerge("hover:empty:p-2 hover:empty:p-3")).toBe("hover:empty:p-3");
6 | expect(twMerge("read-only:p-2 read-only:p-3")).toBe("read-only:p-3");
7 | });
8 |
9 | test("handles pseudo variant group conflicts properly", () => {
10 | expect(twMerge("group-empty:p-2 group-empty:p-3")).toBe("group-empty:p-3");
11 | expect(twMerge("peer-empty:p-2 peer-empty:p-3")).toBe("peer-empty:p-3");
12 | expect(twMerge("group-empty:p-2 peer-empty:p-3")).toBe(
13 | "group-empty:p-2 peer-empty:p-3"
14 | );
15 | expect(twMerge("hover:group-empty:p-2 hover:group-empty:p-3")).toBe(
16 | "hover:group-empty:p-3"
17 | );
18 | expect(twMerge("group-read-only:p-2 group-read-only:p-3")).toBe(
19 | "group-read-only:p-3"
20 | );
21 | });
22 |
--------------------------------------------------------------------------------
/tests/separators.test.ts:
--------------------------------------------------------------------------------
1 | import { createMerge, tailwind } from "../src";
2 |
3 | test("single character separator working correctly", () => {
4 | const twMerge = createMerge(tailwind(), { separator: "_" });
5 |
6 | expect(twMerge("block hidden")).toBe("hidden");
7 |
8 | expect(twMerge("p-3 p-2")).toBe("p-2");
9 |
10 | expect(twMerge("!right-0 !inset-0")).toBe("!inset-0");
11 |
12 | expect(twMerge("hover_focus_!right-0 focus_hover_!inset-0")).toBe(
13 | "focus_hover_!inset-0"
14 | );
15 | expect(twMerge("hover:focus:!right-0 focus:hover:!inset-0")).toBe(
16 | "hover:focus:!right-0 focus:hover:!inset-0"
17 | );
18 | });
19 |
20 | test("multiple character separator working correctly", () => {
21 | const twMerge = createMerge(tailwind(), { separator: "__" });
22 |
23 | expect(twMerge("block hidden")).toBe("hidden");
24 |
25 | expect(twMerge("p-3 p-2")).toBe("p-2");
26 |
27 | expect(twMerge("!right-0 !inset-0")).toBe("!inset-0");
28 |
29 | expect(twMerge("hover__focus__!right-0 focus__hover__!inset-0")).toBe(
30 | "focus__hover__!inset-0"
31 | );
32 | expect(twMerge("hover:focus:!right-0 focus:hover:!inset-0")).toBe(
33 | "hover:focus:!right-0 focus:hover:!inset-0"
34 | );
35 | });
36 |
--------------------------------------------------------------------------------
/tests/standalone-classes.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("merges standalone classes from same group correctly", () => {
4 | expect(twMerge("inline block")).toBe("block");
5 | expect(twMerge("hover:block hover:inline")).toBe("hover:inline");
6 | expect(twMerge("hover:block hover:block")).toBe("hover:block");
7 | expect(
8 | twMerge("inline hover:inline focus:inline hover:block hover:focus:block")
9 | ).toBe("inline focus:inline hover:block hover:focus:block");
10 | expect(twMerge("underline line-through")).toBe("line-through");
11 | expect(twMerge("line-through no-underline")).toBe("no-underline");
12 | });
13 |
--------------------------------------------------------------------------------
/tests/tailwind-css-versions.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("supports Tailwind CSS v3.3 features", () => {
4 | expect(twMerge("text-red text-lg/7 text-lg/8")).toBe("text-red text-lg/8");
5 | expect(
6 | twMerge(
7 | [
8 | "start-0 start-1",
9 | "end-0 end-1",
10 | "ps-0 ps-1 pe-0 pe-1",
11 | "ms-0 ms-1 me-0 me-1",
12 | "rounded-s-sm rounded-s-md rounded-e-sm rounded-e-md",
13 | "rounded-ss-sm rounded-ss-md rounded-ee-sm rounded-ee-md",
14 | ].join(" ")
15 | )
16 | ).toBe(
17 | "start-1 end-1 ps-1 pe-1 ms-1 me-1 rounded-s-md rounded-e-md rounded-ss-md rounded-ee-md"
18 | );
19 | expect(
20 | twMerge(
21 | "start-0 end-0 inset-0 ps-0 pe-0 p-0 ms-0 me-0 m-0 rounded-ss rounded-es rounded-s"
22 | )
23 | ).toBe("inset-0 p-0 m-0 rounded-s");
24 | expect(twMerge("hyphens-auto hyphens-manual")).toBe("hyphens-manual");
25 | expect(
26 | twMerge(
27 | "from-0% from-10% from-[12.5%] via-0% via-10% via-[12.5%] to-0% to-10% to-[12.5%]"
28 | )
29 | ).toBe("from-[12.5%] via-[12.5%] to-[12.5%]");
30 | expect(twMerge("from-0% from-red")).toBe("from-0% from-red");
31 | expect(
32 | twMerge(
33 | "list-image-none list-image-[url(./my-image.png)] list-image-[var(--value)]"
34 | )
35 | ).toBe("list-image-[var(--value)]");
36 | expect(twMerge("caption-top caption-bottom")).toBe("caption-bottom");
37 | expect(twMerge("line-clamp-2 line-clamp-none line-clamp-[10]")).toBe(
38 | "line-clamp-[10]"
39 | );
40 | expect(twMerge("delay-150 delay-0 duration-150 duration-0")).toBe(
41 | "delay-0 duration-0"
42 | );
43 | expect(twMerge("justify-normal justify-center justify-stretch")).toBe(
44 | "justify-stretch"
45 | );
46 | expect(twMerge("content-normal content-center content-stretch")).toBe(
47 | "content-stretch"
48 | );
49 | expect(twMerge("whitespace-nowrap whitespace-break-spaces")).toBe(
50 | "whitespace-break-spaces"
51 | );
52 | });
53 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": ["./**/*"]
4 | }
5 |
--------------------------------------------------------------------------------
/tests/tw-merge.test.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from "../src";
2 |
3 | test("twMerge", () => {
4 | expect(twMerge("mix-blend-normal mix-blend-multiply")).toBe(
5 | "mix-blend-multiply"
6 | );
7 | expect(twMerge("h-10 h-min")).toBe("h-min");
8 | expect(twMerge("stroke-black stroke-1")).toBe("stroke-black stroke-1");
9 | expect(twMerge("stroke-2 stroke-[3]")).toBe("stroke-[3]");
10 | expect(twMerge("outline outline-black outline-1")).toBe(
11 | "outline outline-black outline-1"
12 | );
13 | expect(twMerge("outline outline-dashed")).toBe("outline-dashed");
14 | expect(twMerge("outline-dashed outline-black outline")).toBe(
15 | "outline-black outline"
16 | );
17 | expect(twMerge("grayscale-0 grayscale-[50%]")).toBe("grayscale-[50%]");
18 | expect(twMerge("grow grow-[2]")).toBe("grow-[2]");
19 | expect(
20 | twMerge("bg-blue-500 bg-gradient-to-t bg-origin-top bg-none bg-red-500")
21 | ).toBe("bg-origin-top bg-none bg-red-500");
22 | expect(twMerge("bg-blue-500 bg-none bg-origin-top bg-gradient-to-t")).toBe(
23 | "bg-blue-500 bg-origin-top bg-gradient-to-t"
24 | );
25 | });
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Type Checking
4 | "allowUnreachableCode": false,
5 | "allowUnusedLabels": false,
6 | "noFallthroughCasesInSwitch": true,
7 | "noImplicitOverride": true,
8 | "noUncheckedIndexedAccess": true,
9 | "strict": true,
10 | // Modules
11 | "module": "ESNext",
12 | "moduleResolution": "Node",
13 | // Emit
14 | "declaration": true,
15 | "noEmit": true,
16 | "outDir": "dist",
17 | "sourceMap": true,
18 | // Interop Constraints
19 | "esModuleInterop": true,
20 | "forceConsistentCasingInFileNames": true,
21 | // Language and Environment
22 | "target": "ESNext"
23 | },
24 | "include": ["src/**/*"]
25 | }
26 |
--------------------------------------------------------------------------------