tw-in-js / twind

The smallest, fastest, most feature complete Tailwind-in-JS solution in existence.
https://twind.style
MIT License
3.76k stars 100 forks source link

Force twind shim to render sheet with all classes even if not present in the dom #155

Closed jordwalke closed 2 years ago

jordwalke commented 3 years ago

Hi, I'm building a utility that will allow you to use twind to render multiple different versions of the style sheet (I'm doing so in an iframe as I have found that twind is pretty "global" in nature - yes, I've read all of the examples about .reset()). What I'm doing right now with rendering twind in an iframe to force isolation works fine. I render the domSheet() in an iframe and report back the generated stylesheet to the main document. This allows me to non-destructively/statelessly try out different configs (almost like hot reloading). The problem is that the generated stylesheet will only render classes that are actually used in the document. Is there a way to tell the setup() to render all classes even if they're not used yet?

I have also tried using the "non shim" version of the module to no avail. Here's my example:

            import *  as twindShim from 'https://cdn.skypack.dev/twind/shim';
            twindShim.setup({
              preflight: true, // do not include base style reset (default: use tailwind preflight)
              mode: 'warn', // throw errors for invalid rules (default: warn)
              hash: false, // hash all generated class names (default: false)
              theme: {
                colors: MY_COLORS
              }, // define custom theme values (default: tailwind theme)
              darkMode: 'class', // use a different dark mode strategy (default: 'media')
              sheet: domSheet()
            });
            window.parent.postMessage(
              {
                messageType: "styleData",
                styleInnerHTML: document.querySelectorAll('style')[0].innerHTML
              },
              "*"
            );
danielweck commented 3 years ago

This feature could also be used at development time to get auto-completion of all possible Tailwind / Twind CSS classe names directly in the web browser / web inspector.

Now, I must say that I wouldn't personally use this development workflow, because I use a build system with near-instant refresh (Preact WMR). But in cases where the DX is slower, the ability to try out styles directly in the web inspector (i.e. without any compile step whatsoever) may be desirable. Caveat: once the classes are entered in the live DOM, they have to somehow be copy/pasted back into the source JSX/whatever ... that's one more reason why I wouldn't personally bother relying on this workflow, but perhaps it would help some developers out there :)

jordwalke commented 3 years ago

My workflow doesn't use a build, watcher, or server so that's why it's useful to me (I hope to others as well).

Caveat: once the classes are entered in the live DOM, they have to somehow be copy/pasted back into the source JSX/whatever ...

I'm not sure what you meant, and it might be that I miscommunicated. My goal is to just get a single css stylesheet for all the tailwind classes (including dark mode), along with my customizations to the theme. Once I obtain that, I would inject it into the doc as a <style> tag, and then manually reference those classes as in <div class="bg-blue-100"> etc. Did I communicate that part effectively?

jordwalke commented 3 years ago

The shim seems to generate that css file - but only for the classnames that are currently referenced in the current DOM. I want to generate it "offline" without knowing what will end up in the DOM, and then use twind without the twind runtime in production, which also gives me the ability to generate multiple themes ahead of time and switch between them by replacing the <style> tag. I assume twind has the set of all valid classes somewhere since it is able to warn/error when referencing an unknown class. It stands to reason it could simply generate all of them based on that information into a style sheet.

danielweck commented 3 years ago

My goal is to just get a single css stylesheet for all the tailwind classes...

Sorry, my bad for not being clear: I meant that being able to generate a complete stylesheet of all possible outcomes based on a given Twind configuration (i.e. preflight, plugins, theme etc.) would open another useful development-time feature.

danielweck commented 3 years ago

and then use twind without the twind runtime in production,

That's also my goal in a side project / experiment with Preact WMR static SSR + hydration (see my discussion thread in this repo, it is actually quite a complex issue, depending on the dev/prod framework).

However I would never want to ship a full unpruned stylesheet in my webapp, as this would negate all the byte savings I am aiming for everywhere else :) And introducing a pruning stage would basically be like working with TailwindCSS (before they introduced their Just In Time compiler), so not really in line with Twind's design principles.

In your use case, do you really intend to ship all TailwindCSS classes in your final production build, or just at development time? (sorry if I misunderstand your goals)

jordwalke commented 3 years ago

@danielweck I would probably ship all of the css classes, and only later prune them if it became a bottleneck. With dynamically rendered apps it's very hard to determine which classes will certainly not be rendered. From the root of the app all classes are theoretically "reachable". There are framework level solutions which probably do a good job, but I'm creating a new framework so existing out of the box ones won't work for me.

Is there a way to do what I want with tailwind entirely in the browser? I was interested in twind because it appeared to offer a browser-only solution where themes can also be built in browser, and then also some of the "escape hatches" twind has to use JS when necessary (if I ever need it).

danielweck commented 3 years ago

just as a point of reference relating to the dev time feature i was describing, see:

https://github.com/windicss/vite-plugin-windicss#design-in-devtools and https://twitter.com/antfu7/status/1372368366179028996?s=21

(Twind already provides the mutation observer aka shim, all that’s needed is the “full” stylesheet so that class names can be auto completed directly from the web inspector)

45456085-BB34-4E45-AE4A-5FDE5FB1A87D

DCF81178-0ED6-4342-98E4-4B5205522ADE

danielweck commented 3 years ago

Is there a way to do what I want with tailwind entirely in the browser?

Have you looked into Tailwind CSS Just In Time compiler? Sounds like this would hit the sweet spot for you. Although I’m not sure about escape hatches allowing for custom styling (Twind provides a pretty flexible APi, which I value in equal measure to Tailwind’s opinionated design system)

What I am struggling to understand fully is the reason why you are seeking to eliminate Twind runtime whilst you seem ok with including a large stylesheet. Is this motivated by <noscript>?

danielweck commented 3 years ago

I am starting to think/realise that I am going off-tangent with my inquisitive comments about Twind runtime :) If I understand correctly, you just want to collect all the styles in your utility app, but performance isn’t your primary concern?

sastan commented 3 years ago

If you do not know which classes will be used the twind runtime and maybe observe/shim are the right way to go.

That leaves us with the implicit global state. Could you render the themed parts of your site within a web component? That would prevent leaking of global styles. Each component would have its own twind instance and stylesheet.

The example in the docs uses tw but twind/observe should work just as well. It would create styles for all elements with that component.

https://twind.dev/docs/handbook/recipes/use-with/web-components.html

sastan commented 3 years ago

@danielweck Please do not forget about @twind/cli. Our tool to generate a CSS file - works just like the new tailwindcss jit, but without postcss

danielweck commented 3 years ago

@danielweck Please do not forget about @twind/cli. Our tool to generate a CSS file - works just like the new tailwindcss jit, but without postcss

On that point:

https://github.com/tw-in-js/twind-cli/issues/1

;)

danielweck commented 3 years ago

Please do not forget about @twind/cli. Our tool to generate a CSS file

Sure, but @jordwalke stated:

The problem is that the generated stylesheet will only render classes that are actually used in the document. Is there a way to tell the setup() to render all classes even if they're not used yet?

sastan commented 3 years ago

That leaves: https://github.com/tw-in-js/twind/issues/155#issuecomment-804827343

jordwalke commented 3 years ago

That leaves us with the implicit global state. Could you render the themed parts of your site within a web component? That would prevent leaking of global styles. Each component would have its own twind instance and stylesheet.

That is pretty much what I am doing with an iframe. I'm just wondering why there is even global state in twind.

jordwalke commented 3 years ago

I'll look into the cli, but it's idea to do this in the browser. (And there doesn't appear to be a way to have the cli generate all class names for markup that is not yet generated) The reason why I am okay with having a large css file is that I can generate the entire thing at dev time with no build step or dev server (designer friendly), and then later trim out dead css after rendering the page.

sastan commented 3 years ago

I'm just wondering why there is even global state in twind.

There can be several twind instances (tw) but we expose one as the default via import {tw} from 'twind'. This one can be configured via the setup export. This way you can write use components from different packages without worrying about passing the right instance around. Component libraries use the default tw import and apps use setup for customization.

The instance is responsible for generating the styles and keep them in the right order within its style sheet. It not simply appending styles that would not work with most utilities. Because the order within the stylesheet is important.

sastan commented 3 years ago

Could please help me understand your problem better:

  1. Do you know which classes should be generated?
  2. Should all possible classes including variants permutations be generated?
jordwalke commented 3 years ago

Okay, I think that makes sense. I tried instances but it didn't work and I thought somehow instances were "broken" when customizing the theme but now I think that's not the case - it's just that I didn't have any DOM nodes in the page that used the classes for twind to discover which is why it wasn't generating the necessary classes.

jordwalke commented 3 years ago

I tried minifying the entire tailwind generated css file, and after compressing with gzip it's only 300kb. That doesn't seem extreme for many use cases. In some cases, the simplicity of not having to run a build step, watcher, server would be worth it imho. Then imagine adding a couple of config values in setup() where you can strip out certain feature you are pretty sure you won't need (either a blacklist or whitelist approach). Wouldn't be surprised if most people could quickly get the file size down to about 150kb without any other worry about dev servers/watchers or build steps. Perhaps not suitable for the fastest site in the world, but universally compatible with any framework, existing or to-be-invented, and very low friction.

For comparison one full res image (when you click on it and open it) on Twitter desktop website is about 600kb compressed. 200kb cached isn't the end of the world for most websites, and there's a ton that can be done after the fact to trim out unneeded css when someone wants to optimize. How many projects even get past the prototyping/iteration stage (maybe 20-30%?)

Edit: I realized that my tailwind generated css with all of the classes didn't include dark mode variants. Maybe that invalidates my analysis above? (I'm not sure how much that adds to the file size, but I imagine the additional size compresses even better than the initial kb)

jordwalke commented 3 years ago

Could please help me understand your problem better:

Do you know which classes should be generated?

No, I do not know at the point of generating the css.

Should all possible classes including variants permutations be generated?

I would say yes - at least for this workflow that accommodates the lowest friction use case. I'm not advocating dropping the size optimization features of twind (those are awesome make-or-break features important for many production cases). I'm just suggesting a very low friction workflow for starting projects. I was surprised at how much friction tailwind css required to setup. I think it's an opportunity for twind to offer an easier on-ramp, while being more flexible, and without sacrificing the more advanced performance sensitive use cases.

sastan commented 3 years ago

Then my question again: Why not use the shim or observe with a custom stylesheet? That would allow generating all styles needed.

Why do you need more styles than being used on the page? I'm struggling to understand what you are trying to achieve.

jordwalke commented 3 years ago

Why not use the shim or observe with a custom stylesheet? That would allow generating all styles needed.

The shim would generate all styles even if they are not present on the page at the time of generation? I need to capture the generated css and store it in a variable, then eliminate the twind runtime. The reason is that I need to do this multiple times for multiple different themes. I tried using the shim, and it only generated style rules for elements that were currently on the page. Sure, if I later add the class names to the dom it will update the stylesheet, but that's not what my use case is. I also need to customize the setup in a way that can't be configured with an inline <script type=twind-config> because I programmatically generate the color pallet.

Why do you need more styles than being used on the page? I'm struggling to understand what you are trying to achieve.

Because I want to eliminate the twind runtime, and I can't anticipate which classes will be necessary ahead of time.

sastan commented 3 years ago

Why not use the shim or observe with a custom stylesheet? That would allow generating all styles needed.

The shim would generate all styles even if they are not present on the page at the time of generation?

No. Only those that are used.

I need to capture the generated css and store it in a variable. The reason is that I need to do this multiple times for multiple different themes. I tried using the shim, and it only generated style rules for elements that were currently on the page. I also need to customize the setup in a way that can't be configured with an inline <script type=twind-config> because I programmatically generate the color pallet.

import { create } from 'twind'
import { virtualSheet, getStyleTag, getStyleTagProperties } from 'twind/sheets'

const sheet = virtualSheet()
const { tw } = create({
  sheet,
  theme: { /* .. */ }
})

// generate all styles here - see next section
tw(allTheStylesArray)

// retrieve the style
getStyleTag(sheet)
// => '<style>...</style>'

const { textContent } = getStyleTagProperties(sheet)
// => just the styles

How to get a list of all style rules? In the typescript plugin we do something like that. Maybe we could extract that as an API. Or at least generate a JSON file with all the classes.

Because I want to eliminate the twind runtime, and I can't anticipate which classes will be necessary ahead of time.

You want to replace a 13kb runtime with 300kb of CSS?

jordwalke commented 3 years ago

You want to replace a 13kb runtime with 300kb of CSS?

Yes, because the runtime is something that costs cpu (correct if I'm wrong) and the css bundle is something that is cached by the browser and can be trimmed with a variety of methods.

sastan commented 3 years ago

The script can be cached by the browser. You are assuming the a 300kb CSS files has benefits over a 13kb JS file?

Who does the trimming when?

Have you done any performance tests that may indicate a problem?

jordwalke commented 3 years ago

The script also executes additional instructions, no? Maybe my understanding is incorrect but I believed that the twind runtime would execute code when the page starts up or when elements are appended to the DOM. (Compared to a css file which is cached by the browser, and wouldn't block the execution of other JS instructions).

How to get a list of all style rules? In the typescript plugin we do something like that. Maybe we could extract that as an API.

This seems to be the missing piece. If I had that list I wouldn't need anything else from twind in the first place because I would just make a <div> with all those classes before setting up tailwind, then grab all the generated styles.

jordwalke commented 3 years ago

Do you have a link to where the list of all the style rules (or at least the list of class names that twind will generate style rules for?)

sastan commented 3 years ago

The script also executes additional instructions, no? Maybe my understanding is incorrect but I believed that the twind runtime would execute code when the page starts up or when elements are appended to the DOM. (Compared to a css file which is cached by the browser, and wouldn't block the execution of other JS instructions).

Have you done any performance tests that may indicate a problem?

The CSS file must be parsed as well! A full Tailwind CSS file has blown up several browser tabs already.

Who does the trimming and when?

sastan commented 3 years ago

Do you have a link to where the list of all the style rules (or at least the list of class names that twind will generate style rules for?)

Which variants do you need? All? In which combinations? :hover:focus:disabled?

jordwalke commented 3 years ago

@sastan It would be nice to pass a list of variants, and have the set of classnames for those variants. tw(classesForVariants('hover', 'focus', 'focus-in', 'dark', 'md')) etc.

But I'd even take all possible variants to start if that's the only option.

jordwalke commented 3 years ago

The CSS file must be parsed as well! A full Tailwind CSS file has blown up several browser tabs already.

That is true. But I was thinking that trimming unneeded css is a solved problem in general (not even framework specific). trim(cssFile, myHTML) == trimmedCSS.

My argument for the tradeoff in file size was not "I'd rather have the entire tailwind css file instead of the runtime of twind because it's smaller and faster" it was "I want the simplest possible developer workflow with no build step for multiple themes that I can switch between (computed in the browser without any cli dev tools or build steps), and I'll take a file size hit in development mode because it's not that bad for most use cases and trimming the unneeded style rules out later has many solutions and should be a solved problem in general that I assume is solved somewhere in a css-framework agnostic way"

sastan commented 3 years ago

Do I understand correctly that this would be the flow:

  1. generate all CSS
  2. develop
  3. purge the CSS
  4. use purged CSS in production

Why not use the twind + shim in development? We can figure out the theming stuff.

And for production use the shim to extract all required rules to generate a static CSS file.

sastan commented 3 years ago

It would be nice to pass a list of variants, and have the set of classnames for those variants. tw(classesForVariants('hover', 'focus', 'focus-in', 'dark', 'md')) etc.

That would be quite restrictive.

But I'd even take all possible variants to start if that's the only option.

That would be mean several 10.000 CSS rules!

sastan commented 3 years ago

I want the simplest possible developer workflow with no build step for multiple themes

And that is the twind/shim. Could you show me some performance stats that show an impact?

jordwalke commented 3 years ago

Regarding concerns for file size: For development mode: A 6mb css file takes 100ms to parse by the browser and is served "locally" off my file system in about 30ms. The size is not a concern for development mode imho.

sastan commented 3 years ago

How about parsing and executing twind?

I really do not get what the problem is. There is simply an existing solution why not use it?

jordwalke commented 3 years ago

Why not use the twind + shim in development? We can figure out the theming stuff.

And for production use the shim to extract all required rules to generate a static CSS file.

It wasn't clear how to do this in a way that allows:

Maybe it's documented but it wasn't clear to me.

sastan commented 3 years ago

extract all of the styles that will possibly be necessary depending on which interactions will be performed

If this is so highly dynamic why not use the shim in production and be done with it.

jordwalke commented 3 years ago

It seems to me that if one wants to pre-render a stylesheet, the only way to do so with 100% accuracy and not knowing which interactions/frameworks might use which css classes is to take the entire stylesheet with all the variants, which brings me back to the original problem. The only solutions here seem to be: A) to include the twind runtime even in production, or B) to render out all possible classes. Some in this thread have suggested some other third alternative - which is to use twind in development mode, but then somehow take pre-computed stylesheets for production. But that's what I was after in the first place. That third option is the missing feature/use case which I am after.

sastan commented 3 years ago

You talked about a simple no build step process. But want to create a large css file with thousands of rules and then purge it.

Compare that with import twind/shim. Could you show me some performance stats that show an impact?

Edit: We have a twind/shim/server module that parses HTML string and generates all styles within it.

jordwalke commented 3 years ago

You talked about a simple no build step process.

For development mode.

For production mode the build step would be done automatically when the user does Chrome "Save As". It's a very different workflow, so I understand why this might be a round peg in a square hole for twind.

sastan commented 3 years ago

But then use them shim in development mode. And then twind/shim/server to parse the HTML to generate the styles.

Edit: It is called server but works in the browser

jordwalke commented 3 years ago

But then use them shim in development mode. And then twind/shim/server to parse the HTML to generate the styles.

I would like to do that but it wasn't clear it was even possible to generate multiple themes using only the shim, and toggle between those themes. Edit: I seemed to have run into some global state issues. Maybe I was just using it incorrectly?

sastan commented 3 years ago

With https://twind.dev/docs/modules/twind_observe.html you can do that. That is the base module for the shim.

jordwalke commented 3 years ago

Even with the server mode, I think I run into issues where there is markup not yet rendered on the page, so it won't generate all the necessary rules.

sastan commented 3 years ago

How would then purged the styles?

jordwalke commented 3 years ago

I was thinking of some kind of a config where I would "opt in" to various segments of tailwind features. Like "spacing"(includes flex), "colors", and variants ("focus", "focus-in") etc. And it would notify me at dev time with an error telling me I'm using classes that I haven't opted into.

sastan commented 3 years ago

I still recommend the shim for production you could inline these UMD files

<scrip src="https://unpkg.com/twind/twind.umd.js"></script>
<script src="https://unpkg.com/twind/observe/observe.umd.js"></script>
<script>
  twind.setup({ theme: {}})
  twindObserver.observe(document.documentElement)
</script>

Or create a bundle. The would be executed in the head. We are already doing something like this. And the lighthouse score is 100.

jordwalke commented 3 years ago

@sastan Is there a way with the shim to be able to later reconfigure the setup/theme? I tried this and my naive approach did not work. The second setup/theme did nothing. The use case is that I wanted to have an entirely different theme depending on whether the user was in dark mode or not (I would use that to switch the theme). The issue with tailwind is that the color pallets aren't really easily customizable based on whether or not you are in dark mode. The requirements for normalizing color contrast in different dark/light modes are different.