Skip to content

Commit

Permalink
Merge pull request #63 from dcastil/feature/add-support-for-tailwind-v3
Browse files Browse the repository at this point in the history
Add support for Tailwind v3
  • Loading branch information
dcastil committed Dec 10, 2021
2 parents b8e47c8 + 1f799bc commit bf4b842
Show file tree
Hide file tree
Showing 11 changed files with 641 additions and 284 deletions.
51 changes: 34 additions & 17 deletions README.md
Expand Up @@ -16,17 +16,11 @@ twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
// → 'hover:bg-dark-red p-3 bg-[#B91C1C]'
```

- Supports Tailwind v2.0 up to v2.2, support for newer versions will be added continuously
- Supports Tailwind v3.0 (if you use Tailwind v2, use [tailwind-merge v0.9.0](https://github.com/dcastil/tailwind-merge/tree/v0.9.0))
- Works in Node >=12 and all modern browsers
- Fully typed
- [<!-- AUTOGENERATED START package-gzip-size -->5.2 kB<!-- AUTOGENERATED END --> minified + gzipped](https://bundlephobia.com/package/tailwind-merge) (<!-- AUTOGENERATED START package-composition -->96.7% self, 3.3% hashlru<!-- AUTOGENERATED END -->)

## Early development

This library is in an early pre-v1 development stage and might have some bugs and inconveniences here and there. I use the library in production and intend it to be sufficient for production use, as long as you're fine with some potential breaking changes in minor releases until v1 (lock the version range to patch releases with `~` in your `package.json` to prevent accidental breaking changes).

I want to keep the library on v0 until I feel confident enough that there aren't any major bugs or flaws in its API and implementation. If you find a bug or something you don't like, please [submit an issue](https://github.com/dcastil/tailwind-merge/issues/new/choose) or a pull request. I'm happy about any kind of feedback!

## What is it for

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.
Expand Down Expand Up @@ -100,13 +94,26 @@ twMerge('hover:p-2 hover:p-4') // → 'hover:p-4'
twMerge('hover:focus:p-2 focus:hover:p-4') // → 'focus:hover:p-4'
```

### Supports custom values
### Supports arbitrary values

```ts
twMerge('bg-black bg-[color:var(--mystery-var)]') // → 'bg-[color:var(--mystery-var)]'
twMerge('grid-cols-[1fr,auto] grid-cols-2') // → 'grid-cols-2'
```

### Supports arbitrary properties

```ts
twMerge('[mask-type:luminance] [mask-type:alpha]') // → '[mask-type:alpha]'
twMerge('[--scroll-offset:56px] lg:[--scroll-offset:44px]')
// → '[--scroll-offset:56px] lg:[--scroll-offset:44px]'

// Don't do this!
twMerge('[padding:1rem] p-8') // → '[padding:1rem] p-8'
```

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.

### Supports important modifier

```ts
Expand Down Expand Up @@ -193,13 +200,13 @@ E.g. here is the overflow class group which results in the classes `overflow-aut
const overflowClassGroup = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }]
```

Sometimes it isn't possible to enumerate all elements in a class group. Think of a Tailwind class which allows custom 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.
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.

E.g. here is the fill class group.

```ts
const isCustomValue = (classPart: string) => /^\[.+\]$/.test(classPart)
const fillClassGroup = [{ fill: ['current', isCustomValue] }]
const isArbitraryValue = (classPart: string) => /^\[.+\]$/.test(classPart)
const fillClassGroup = [{ fill: ['current', isArbitraryValue] }]
```

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](#validators), so you don't need to recreate them.
Expand Down Expand Up @@ -506,9 +513,14 @@ const customTwMerge = createTailwindMerge(getDefaultConfig, (config) =>
```ts
interface Validators {
isLength(classPart: string): boolean
isCustomLength(classPart: string): boolean
isArbitraryLength(classPart: string): boolean
isInteger(classPart: string): boolean
isCustomValue(classPart: string): boolean
isArbitraryValue(classPart: string): boolean
isTshirtSize(classPart: string): boolean
isArbitrarySize(classPart: string): boolean
isArbitraryPosition(classPart: string): boolean
isArbitraryUrl(classPart: string): boolean
isArbitraryWeight(classPart: string): boolean
isAny(classPart: string): boolean
}
```
Expand All @@ -521,10 +533,15 @@ const paddingClassGroup = [{ p: [validators.isLength] }]

A brief summary for each validator:

- `isLength` checks whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), a custom length (`[3%]`, `[4px]`, `[length:var(--my-var)]`), or one of the strings `px`, `full` or `screen`.
- `isCustomLength` checks for custom length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`).
- `isInteger` checks for integer values (`3`) and custom integer values (`[3]`).
- `isCustomValue` checks whether the class part is enclosed in brackets (`[something]`)
- `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`.
- `isArbitraryLength` checks for arbitrary length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`).
- `isInteger` checks for integer values (`3`) and arbitrary integer values (`[3]`).
- `isArbitraryValue` checks whether the class part is enclosed in brackets (`[something]`)
- `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`).
- `isArbitrarySize` checks whether class part is arbitrary value which starts with with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames.
- `isArbitraryPosition` checks whether class part is arbitrary value which starts with with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames.
- `isArbitraryUrl` checks whether class part is 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.
- `isArbitraryWeight` checks whether class part is arbitrary value whcih starts with `weight:` or is a number (`[weight:var(--value)]`, `[450]`) which is necessary for font-weight classNames.
- `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 ceertain there are no other class groups in a namespace.

### `Config`
Expand Down
19 changes: 18 additions & 1 deletion src/class-utils.ts
Expand Up @@ -24,7 +24,7 @@ export function createClassUtils(config: Config) {
classParts.shift()
}

return getGroupRecursive(classParts, classMap)
return getGroupRecursive(classParts, classMap) || getGroupIdForArbitraryProperty(className)
}

function getConflictingClassGroupIds(classGroupId: ClassGroupId) {
Expand Down Expand Up @@ -64,6 +64,23 @@ function getGroupRecursive(
return classPartObject.validators.find(({ validator }) => validator(classRest))?.classGroupId
}

const arbitraryPropertyRegex = /^\[(.+)\]$/

function getGroupIdForArbitraryProperty(className: string) {
if (arbitraryPropertyRegex.test(className)) {
const arbitraryPropertyClassName = arbitraryPropertyRegex.exec(className)![1]
const property = arbitraryPropertyClassName?.substring(
0,
arbitraryPropertyClassName.indexOf(':')
)

if (property) {
// I use two dots here because one dot is used as prefix for class groups in plugins
return 'arbitrary..' + property
}
}
}

/**
* Exported for testing only
*/
Expand Down

0 comments on commit bf4b842

Please sign in to comment.