Replies: 1 comment
-
|
Testing. Somehow can't find this in the discussion |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
This discussion tries to create a proposal that describes how to create and consume a component library written with Tailwind CSS in a way that stays consistent with CSS order of appearance and can be consumed by any application, including complex applications like MFE with runtime component injection.
Problem Summary
There are many challenges to ship a Tailwind-based component library that “just works” in any app.
dark:,rtl:,reduced-motion:), but how those variants are implemented (selector strategy, media query, etc.) varies from application to application.tailwind-mergeis needed to remove conflicting classes. However, there are limitations to the typicalclassName: stringapproach.1. CSS definition conflicts
Different libraries (or the same library in different versions) can emit different CSS for the same Tailwind class name. For example, one build might define
rounded-lgasborder-radius: 0.5remwhile another uses0.75rem; orbg-primarymight map to different theme values. When an app composes multiple Tailwind-based libraries or overrides the theme, the same class name can therefore resolve to different computed styles. That leads to visual inconsistency and bugs that are hard to track down, because the HTML and class names look correct—only the underlying CSS differs.Note
Solution:
Each library uses its own prefix to avoid conflicts.
When an app wants to customize the library's CSS, it can either:
2. CSS order of appearance
When multiple libraries (or the application itself) are using the same Tailwind classes, the order of appearance of the CSS rules determines which rule wins, regardless of the order of the CSS classes in the HTML.
for example, in the following code:
Only one of the two divs will be colored correctly, depending on the order of the CSS rules.
This means when a library is develop in isolation, the order of the CSS rules is not guaranteed to be the same as when the library is used in the application.
Note
Solution:
Each library uses its own prefix to avoid conflicts.
Use class name merging tools such as
⚠️ merging tools doesn't work well with multiple prefixes and unknown prefixes.
tailwind-mergeto remove conflicting classes.3. Variant usage vs. variant mechanism
A library declares variant usage when it uses classes like
dark:bg-zinc-800orrtl:ml-4—it is saying “this component depends on adark(orrtl) variant being available.”The variant mechanism is how that variant is implemented: e.g. a class on an ancestor (
.dark), a media query (prefers-color-scheme: dark), or a data attribute. That choice is application-specific.If the library ships pre-built CSS, it was built with one mechanism; the app might use another. Then the library’s
dark:rules might never apply, or might conflict with the app’s dark-mode strategy. So the library should only depend on the existence of named variants; the application must own how those variants are defined so that a single build can use one consistent mechanism.Warning
Solution:
The library should share its Tailwind configuration,
and potentially its source code.
The configuration should not include preflight.
The application's Tailwind configuration should include the library's Tailwind configuration, so that the application can define the variant mechanism and generate the actual CSS.
The application need to create a separate config for each component library (and its transient component libraries) and setup the
@sourcemanually pointing to the source undernode_modules.💡 Alternatively, use
postcssto alter the variant mechanism of the library's CSS.4. Tailwind prefix merge
With component libraries (and the application itself) using their own prefixes, there is a need to be able to merge Tailwind classes of different prefixes.
There is a way to do it, but it requires the
twMergefunction to know all prefixes ahead of time.However, the
twMergeis defined and used by each component library. This is because typically the propclassNameis of typestring, so the merge can only happen within the component.Warning
Solution:
twMergecan detect and merge conflicting classes correctly.This is not a complete solution, as there could be custom
@utilitiesthat requires further customization to thetwMergefunction.To solve that, then the
tailwind-mergepackage needs to be able to be configured globally, using some global storage (module scope is not enough as there could be multiple instances of the package).classNametype to a function(state: { className: string } & OtherStates) => string.This would allow the
twMergeto be defined and used at the application level, which knows about all prefixes in use.react-aria-components support this, and I'm generalizing it in @just-web/toolkit (WIP).
We can also define a convention for component libraries to expose how to customize the
twMergefunction.Other considerations
When the Library Might Define Variants
data-state="open") and documents it as “add this to your app’s CSS if you use our components,” that’s still the app including a snippet. So the “mechanism” is still in the app’s stylesheet; the library only specifies the contract.@custom-variant dark ...in a non-emitted file (e.g. “copy this into your app”) or in a preset that the app explicitly merges. Then the app can replace it with its own. So the app remains in control; the library only suggests a default. or Tailwind will override the@custom-variantdeclaration based on the order of appearance in the Tailwind config.Cascade Layers
Each library can add their CSS in a dedicated layer, so that the order of appearance is determined by the layer, not by the order of the CSS files. This decision can be made at the component library level, or at the application level.
If the component library wants to specify the layer its CSS are placed, that's supported.
❓One question is what's the recommendation approach on defining the layers in a component library. Using the
theme + utilitiesexample above, there are a few possibilities:layer(theme),layer(utilities)layer(components)layer(utilities)layer(mylib)layer(mylib-theme),layer(mylib-utilities)layer(mylib.theme)(orlayer(mylib)),layer(mylib.utilities)We want the component library to provide a Tailwind config with their own
@source, but@sourcedoes not work within@layer:This can be an additional enhancement to handle the case when two component libraries happens to use the same prefix (or the application needs to load multiple versions of the same library).
One way to make it work today is that the component libraries do not include
@sourceand@custom-variantsin the provided Tailwind config, and the application provide them pointing to the code in the library:Solution
Component libraries should:
@sourcefrom the bundled code)tailwind-mergeconfiguration, or a standardized merge configuration.classNameto accept a callback.Application should:
@custom-variantdeclarations for each variant the application and the component libraries use.classNameto customize the Tailwind classes when merging is needed.Tailwind should:
@tailwindcss/viteand@tailwindcss/postcss.@layerwith@sourcesupport.tailwind-mergeor other tools to merge conflicting classes.Beta Was this translation helpful? Give feedback.
All reactions