Skip to content

Commit

Permalink
Merge pull request #230 from dcastil/feature/143/add-docs-about-when-…
Browse files Browse the repository at this point in the history
…to-use-tailwind-merge

Add docs about when to use tailwind-merge
  • Loading branch information
dcastil authored Jun 1, 2023
2 parents 21e2b9d + 0a9a0ce commit 61fd66f
Show file tree
Hide file tree
Showing 21 changed files with 311 additions and 80 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"extends": ["plugin:jest/recommended", "plugin:jest/style"]
},
{
"files": ["scripts/**/*.?(m)js"],
"files": ["scripts/**/*.?(m|c)js"],
"rules": {
"no-console": "off"
}
Expand Down
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ contact_links:
- name: Bug Report
url: https:/dcastil/tailwind-merge/issues/new
about: If something is broken with tailwind-merge itself, create a bug report.
- name: Start discussion
url: https:/dcastil/tailwind-merge/discussions
about: Anything else on your mind? Check out the discussions forum.
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<!-- This file is autogenerated. If you want to change this content, please do the changes in `./docs/README.md` instead. -->

<div align="center">
<br />
<a href="https:/dcastil/tailwind-merge">
<!-- AUTOGENERATED VERSION START -->
<img src="https:/dcastil/tailwind-merge/raw/v1.12.0/assets/logo.svg" alt="tailwind-merge" height="150px" />
<!-- AUTOGENERATED END -->
</a>
</div>

Expand All @@ -25,16 +25,12 @@ twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')

## Get started

<!-- AUTOGENERATED VERSION START -->

- [What is it for](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/what-is-it-for.md)
- [Features](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/features.md)
- [Configuration](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/configuration.md)
- [Recipes](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/recipes.md)
- [API reference](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/api-reference.md)
- [Writing plugins](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/writing-plugins.md)
- [Versioning](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/versioning.md)
- [Contributing](https:/dcastil/tailwind-merge/tree/v1.12.0/.github/CONTRIBUTING.md)
- [Contributing](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/contributing.md)
- [Similar packages](https:/dcastil/tailwind-merge/tree/v1.12.0/docs/similar-packages.md)

<!-- AUTOGENERATED END -->
35 changes: 35 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<div align="center">
<br />
<a href="https:/dcastil/tailwind-merge">
<img src="../assets/logo.svg" alt="tailwind-merge" height="150px" />
</a>
</div>

# tailwind-merge

Utility function to efficiently merge [Tailwind CSS](https://tailwindcss.com) classes in JS without style conflicts.

```ts
import { twMerge } from 'tailwind-merge'

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 v3.0 up to v3.3 (if you use Tailwind v2, use [tailwind-merge v0.9.0](https:/dcastil/tailwind-merge/tree/v0.9.0))
- Works in all modern browsers and Node >=12
- Fully typed
- [Check bundle size on Bundlephobia](https://bundlephobia.com/package/tailwind-merge)

## Get started

- [What is it for](./what-is-it-for.md)
- [When and how to use it](./when-and-how-to-use-it.md)
- [Features](./features.md)
- [Configuration](./configuration.md)
- [Recipes](./recipes.md)
- [API reference](./api-reference.md)
- [Writing plugins](./writing-plugins.md)
- [Versioning](./versioning.md)
- [Contributing](./contributing.md)
- [Similar packages](./similar-packages.md)
4 changes: 4 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ twJoin(

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.

Why no object support? [Read here](https://github.com/dcastil/tailwind-merge/discussions/137#discussioncomment-3481605).

## `getDefaultConfig`

```ts
Expand Down Expand Up @@ -262,3 +264,5 @@ TypeScript type for config object. Useful if you want to build a `createConfig`
Next: [Writing plugins](./writing-plugins.md)
Previous: [Recipes](./recipes.md)
[Back to overview](./README.md)
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,5 @@ const twMerge3 = createTailwindMerge(() => ({ … }), withMagic, withMoreMagic)
Next: [Recipes](./recipes.md)

Previous: [Features](./features.md)

[Back to overview](./README.md)
2 changes: 2 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Please see [CONTRIBUTING](../.github/CONTRIBUTING.md) for details.
Next: [Similar packages](./similar-packages.md)

Previous: [Versioning](./versioning.md)

[Back to overview](./README.md)
2 changes: 2 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,5 @@ The initial computations are called lazily on the first call to `twMerge` to pre
Next: [Configuration](./configuration.md)

Previous: [What is it for](./what-is-it-for.md)

[Back to overview](./README.md)
2 changes: 2 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,5 @@ function customTwMerge(...inputs) {
Next: [API reference](./api-reference.md)

Previous: [Configuration](./configuration.md)

[Back to overview](./README.md)
2 changes: 2 additions & 0 deletions docs/similar-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
---

Previous: [Contributing](./contributing.md)

[Back to overview](./README.md)
2 changes: 2 additions & 0 deletions docs/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ This package follows the [SemVer](https://semver.org) versioning rules. More spe
Next: [Contributing](./contributing.md)

Previous: [Writing plugins](./writing-plugins.md)

[Back to overview](./README.md)
12 changes: 6 additions & 6 deletions docs/what-is-it-for.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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.
If you use Tailwind CSS 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 a one-off case.

```jsx
// React components with JSX syntax used in this example
Expand All @@ -10,7 +10,7 @@ function MyGenericInput(props) {
return <input {...props} className={className} />
}

function MySlightlyModifiedInput(props) {
function MyOneOffInput(props) {
return (
<MyGenericInput
{...props}
Expand All @@ -20,7 +20,7 @@ function MySlightlyModifiedInput(props) {
}
```

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`.
When `MyOneOffInput` 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`.

This is where tailwind-merge comes in.

Expand All @@ -32,10 +32,10 @@ function MyGenericInput(props) {
}
```

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`.
tailwind-merge overrides conflicting classes and keeps everything else untouched. In the case of the `MyOneOffInput`, the input is now rendered with the classes `border rounded p-3`.

---

Next: [Features](./features.md)
Next: [When and how to use it](./when-and-how-to-use-it.md)

Previous: [Overview](../README.md)
[Back to overview](./README.md)
163 changes: 163 additions & 0 deletions docs/when-and-how-to-use-it.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# When and how to use it

Like any other package, tailwind-merge comes with opportunities and trade-offs. This document tries to help you decide whether tailwind-merge is the right tool for your use case based on my own experience and the feedback I got from the community.

> **Note**
> If you're thinking of a major argument that is not covered here, please [let me know](https:/dcastil/tailwind-merge/discussions/new?category=ideas)!
## Why not to use it

Generally speaking, there are situations where you _could_ use tailwind-merge but probably shouldn't. Think of tailwind-merge as an escape hatch rather than the primary tool to handle style variants.[^simonswiss-quote]

[^simonswiss-quote]: Don't just take my word for it, [Simon Vrachliotis thinks so too](https://twitter.com/simonswiss/status/1663721037949984768).

### Increases bundle size

tailwind-merge relies on a large config (~5 kB out of the ~7 kB minified and gzipped bundle size) to understand which classes are conflicting. This might be limiting if you have tight bundle size constraints.

### Might give too much freedom to users of a component

With large teams or components that are made available publicly you can expect users of components to use and misuse the component's API in any way the component allows. With this in mind tailwind-merge might give too much freedom to users of a component which could make it harder to maintain and evolve the component over time. With tailwind-merge you give up full control over styling in your components.

### More difficult to refactor highly reusable components

When you allow arbitrary classes to be passed into a component, you can break the styles of the component's users when you refactor the component's internal styles. If you need to be able to refactor a component's styles often, those styles shouldn't be merged with styles from props unless you're willing to refactor the component's uses as well.

### Not using Tailwind CSS or component composition

tailwind-merge is probably only useful if you use Tailwind CSS and compose components together in some form. If you have a use case for tailwind-merge outside of thosse boundaries, please [let me know](https:/dcastil/tailwind-merge/discussions/new?category=show-and-tell), I'm curious about it!

## Why to use it

### Easy to compose components through multiple levels

tailwind-merge is a great fit for highly composed components like in design systems or UI component libraries. If you expect that styles of a component will be modified on multiple levels, e.g. ContextMenuOption → MenuOption → BaseOption, with each component passing some modifications to the component it renders, tailwind-merge can help you to keep the API surface between components small.

### Enables fast development velocity and iteration speed

tailwind-merge allows you to support a wide range of styling use cases without having to explicitly define each of them separately within a component. E.g. you can pass a custom width to a button component, change its text color or position it absolutely with a single `className` prop without the need to define support for custom widths, text colors or positioning within the button component explicitly.

### Preventing premature abstractions

Let's say you have a Button component that you already use in many places. You have a place in your app in which you want to make its background red to signal that the action of the button is destructive. You could modify the Button component to deal with the concept of destructiveness (e.g. by passing a `variant` prop with the value `destructive`), but then you'd need to make sure that those styles work with all the other permutations of the component which you don't need in the place where the destructive button is used. And maybe you're not even sure whether you'll keep the Button red in this one place, so the time investment of making the Button understand destructiveness doesn't seem worth it.

tailwind-merge allows you to defer the creation of abstractions like destructiveness to the point where you're sure that you need them. You can just pass a `className` prop to the Button component in which you define the red background and be done with it for now. If you later decide that you want to make the Button red in more places, you can still define the logic inside the Button component later.

## How to use it

### Joining internal classes

If you want to merge classes that are all defined within a component, prefer using the [`twJoin`](./api-reference.md#twjoin) function over [`twMerge`](./api-reference.md#twmerge). As the name suggests, `twJoin` only joins the class strings together and doesn't deal with conflicting classes.

```jsx
// React components with JSX syntax used in this example

function MyComponent({ forceHover, disabled, isMuted }) {
return (
<div
className={twJoin(
TYPOGRAPHY_STYLES_LABEL_SMALL,
'grid w-max gap-2',
forceHover ? 'bg-gray-200' : ['bg-white', !disabled && 'hover:bg-gray-200'],
isMuted && 'text-gray-600',
)}
>
{/* More code… */}
</div>
)
}
```

Joining classes instead of merging forces you to write your code in a way so that no merge conflicts appear which seems like more work at first. But it has two big advantages:

1. It's much more performant because no conflict resolution is computed. `twJoin` has the same performance characteristics as other class joining libraries like [`clsx`](https://www.npmjs.com/package/clsx).

2. It's usually easier to reason about. When you can't override classes, you naturally start to put classes that are in conflict with each other closer together through conditionals like ternaries. Also when a condition within the `twJoin` call is truthy, you can be sure that this class will be applied without the need to check whether conflicting classes appear in a later argument. Not relying on overrides makes it easier to understand which classes are in conflict with each other and which classes are applied in which cases.

But there are also exceptions to (2) in which using `twMerge` for purely internally defined classes is preferable, especially in some complicated cases. So just take this as a rule of thumb.

### Merging internal classes with `className` prop

The primary purpose of tailwind-merge is to merge a `className` prop with the default classes of a component.

```jsx
// React components with JSX syntax used in this example

function MyComponent({ forceHover, disabled, isMuted, className }) {
return (
<div
className={twMerge(
TYPOGRAPHY_STYLES_LABEL_SMALL,
'grid w-max gap-2',
forceHover ? 'bg-gray-200' : ['bg-white', !disabled && 'hover:bg-gray-200'],
isMuted && 'text-gray-600',
className,
)}
>
{/* More code… */}
</div>
)
}
```

You don't need to worry about potentially expensive re-renders here because tailwind-merge [caches results](./features.md#results-are-cached) so that a re-render with the same props and state becomes computationally lightweight as far as the call to `twMerge` goes.

## Alternatives

In case the disadvantages of tailwind-merge weigh in too much for your use case, here are some alternatives that might be a better fit.

### Adding props that toggle internal styles

This is the goold-old way of styling components and is also probably your default. E.g. think of a variant prop that toggles between primary and secondary styles of a button. The `variant` prop is already toggling between internal styles of the component and you can use the same pattern to define any number of styling use cases to a component. If you have a one-off use case to give the button a full width, you can add a `isFullWidth` prop to the button component which toggles the `w-full` class internally.

```jsx
// React components with JSX syntax used in this example

function Button({ variant = 'primary', isFullWidth, ...props }) {
return <button {...props} className={join(BUTTON_VARIANTS[variant], isFullWidth && 'w-full')} />
}

const BUTTON_VARIANTS = {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-200 text-black',
}

function join(...args) {
return args.filter(Boolean).join(' ')
}
```

### Using Tailwind's important modifier

If you have too many different one-off use cases to add a prop for each of them to a component, you can use Tailwind's [important modifier](https://tailwindcss.com/docs/configuration#important-modifier) to override internal styles.

```jsx
// React components with JSX syntax used in this example

function MyComponent() {
return (
<>
<Button className="w-full">No danger</Button>
<Button className="w-full !bg-red-500" >Danger!</Button>
</>
)
}

function Button({ className ...props }) {
return <button {...props} className={join('bg-blue-500 text-white', className)} />
}

function join(...args) {
return args.filter(Boolean).join(' ')
}
```

The main downside of this approach is that it only works one level deep (you can't override the `!bg-red-500` class in the example above). But if you don't need to be able to override styles through multiple levels of composition, this might be the most lightweight approach possible.

---

Next: [Features](./features.md)

Previous: [What is it for](./what-is-it-for.md)

[Back to overview](./README.md)
2 changes: 2 additions & 0 deletions docs/writing-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ Also, feel free to check out [tailwind-merge-rtl-plugin](https://www.npmjs.com/p
Next: [Versioning](./versioning.md)

Previous: [API reference](./api-reference.md)

[Back to overview](./README.md)
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"scripts": {
"build": "dts build",
"test": "dts test",
"test:exports": "node scripts/test-built-package-exports.js && node scripts/test-built-package-exports.mjs",
"test:exports": "node scripts/test-built-package-exports.cjs && node scripts/test-built-package-exports.mjs",
"lint": "eslint --max-warnings 0 '**'",
"size": "size-limit",
"preversion": "if [ -n \"$DANYS_MACHINE\" ]; then git checkout main && git pull; fi",
Expand All @@ -61,7 +61,6 @@
"eslint": "^8.39.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"fp-ts": "^2.14.0",
"globby": "^11.1.0",
"prettier": "^2.8.8",
"size-limit": "^8.2.4",
Expand Down
23 changes: 0 additions & 23 deletions scripts/helpers/apply-versioned-text.js

This file was deleted.

File renamed without changes.
Loading

0 comments on commit 61fd66f

Please sign in to comment.