It would be amazing if/when heex got/gets scope selectors but once again, for the many-th time, that isn’t going to fix the burden of a maintaining generated vanilla CSS without bring NPM back into the mix or other some other option that a good faction of people will complain about. Tailwind literally, and I do mean literally made their cli tool just for frameworks like Phoenix looking to avoid NPM. It’s hard to argue with that kind of dedication from a vendor to use for something that is just meant as a getting start tool and really not meant to make it into production.
But also, I’m not on the Phoenix team, so I’m going to shuduppa my face.
It doesn’t matter if the team were to remove Tailwind and use some names like “modal”, “input-text” or whatnot, there will be a thread the same day asking the classes to be named “the-modal” and “input_for_text”.
On top of that, the implementations of said classes would be up for a fiery debate on what is best. Should a modal be a <div> or a <dialog>. Should the CSS use layers or not, container queries, etc etc.
Picking something like Tailwind alleviates much of that, but more importantly and already mentioned, the default components should not be seen as the end result, it’s a starting point.
Personally I would prefer default components to have no styling at all so I can write highly specific CSS that doesn’t require LiveView to use lists for the class property, but hey, I can do that anyway (and that’s what I’m doing)
Can you expand on where you see this becoming a problem? I actually expected there to be issues like this as well, but in practice it always seems to work out. There are very few styles that actually directly affect children in a way that would cause leakage. There are things like color or font size which are inherited, but that actually turns out to be a feature in my experience.
As an example, if you had a <.button /> component which accepted a slot with <div>Something</div>, you want the div to inherit the font color from the button. And if for some reason you didn’t, you can style that div using the scope of the component calling to override the color. I have written a lot of code like this, and it’s actually great.
Putting aside the technical issues with implementing this in Phoenix, I’m not actually sure full isolation is what you want in practice. It’s good to have an escape hatch when you need it. As long as you use global styles sparingly, maintainability won’t suffer.
You posted this a couple of times and it has me thinking I was not clear enough in my post. What I am proposing is scoped styles which are co-located inside the component heex. The compiler pulls them out using a sigil macro and then compiles them into CSS using a generator. The CSS is handled end-to-end by Elixir code, there is no need for npm stuff.
There is no vanilla CSS file written by the user (unless you want one). Nearly all of my styles are written directly in my component heex. Like Tailwind, this solves the CSS maintainability problem in two ways.
First, when styles are co-located with their components, they are much easier to keep track of with respect to their usage. You know that style is not used elsewhere, and you can delete it when it’s no longer needed. Note that we are solving this problem in almost exactly the same way as Tailwind, except we are using real CSS syntax instead of Tailwind’s degenerate class approach.
Second, since all styles are scoped by default, you can freely re-use class names without fear. This is the other big problem Tailwind solves, and it is a real problem: all of us who have written vanilla CSS know that naming classes kills velocity, and is generally extremely annoying. But when they’re scoped this problem melts away.
I do understand what scoped styles are but it went over my head that it would be fully parsing CSS within Elixir, although I might still be understanding that part? Otherwise you still have the problem with vanilla CSS, I wasn’t specifically talking about a “file.”
In any event, I disagree about your strong language toward Tailwind. I don’t find utility classes to be either degenerate or ugly. Now, they certainly aren’t pretty, but I find it much easier to quickly understand than mentally mapping CSS styles to HTML tags. Obviously you have to multi-line and organize them (so many people seem to be under the impression, perhaps in bad faith, that you must write utility classes in one long line). While I do care about good-looking code, I care about understanding what’s going on as quickly as possible more.
Otherwise, I’ve only used scoped CSS once. In that project we were making components on top of components and adding custom CSS (this was in React). In my current project at work (using HEEx) we are using vanilla CSS and scoping with plain ol’ CSS classes. We have a separate design system repo (we have multi apps) and all CSS lives there, so there is no need for any overrides. We have semantic CSS classes for our components which everything is scoped to and have a sparse few globals (as you touched on) which works quite nicely. To me this isn’t super different than scoped CSS as even though it’s in the same file, it’s not right there so to speak. You’d still need to split view a file to do a side-by-side.
Anyway, sorry this is a bit stream-of-consciousness, I’m replying while waiting on CI
And sorry, I didn’t even click your link. All that said yes, that does look nice!
Exciting to hear that people are still exploring Collocated CSS. There is a PR for collocated hooks and I think it should be generalized for collocated JS (and eventually CSS).
I’d actually love if Tailwind did more than that. Most of the times, I don’t want to decide between 11 shades of 20 colors. In my app, I will probably pick a few: primary, secondary, accent, and use that everywhere with an opacity modifier. Similar for things such as borders, where I want to have three types of borders and a standard thickness. Otherwise it is very easy to define components that are ultimately inconsistent.
I assume that a CSS-based system would achieve this by defining a set of global vars for the colors and borders? However, how would the CSS-based system address the spacing? I guess you could use padding: calc(var(--spacing) * <number>); but writing that every time won’t be fun. Although I heard that custom CSS functions may be in the works?
If global style is only used sparingly, then it does not cost much to duplicate those in scoped styles
If thing keep creeping in global style, then you indeed want the 2-way isolation
IMHO, 2-way isolation is a more robust solution. It should happen on a higher level than functional component and it should be used only when make sense.
That’s the nice thing about Tailwind, though, it’s a tool for building your own utility-class design system, it just comes with a lot defaults. You often end up needing far more shades of the same colours than you think you do, though.
Indeed, I discovered the same thing. In particular, I wanted support for themes in my apps, and not just light/dark. So I created a theming system where you then define more variables in terms of previously-defined variables, and then they can be hotswapped out using a class on the <body>. In practice it looks like this.
I have found that it is rather difficult to design a set of variables that work in both light and dark, as sometimes the direction of lightness/darkness has to be “reversed” arbitrarily based on context. So you end up with more variables than you would think. But it does work - research ongoing
Actually, you may be surprised to hear that I found a spacing scale to be completely unnecessary. Specifying spacing in terms of rem has turned out to be sufficient. I think Tailwind uses a spacing scale mainly because it is descended from a “pure” utility class approach where px-0.5rem would have resulted in too many classes, though of course nowadays they just generate the classes dynamically. Which is, if you really think about it, kind-of insane.
The thing about escape hatches is that you only use them when things are out of your control. For example, here I use the :global escape hatch to style the anchor inside a Phoenix <.link /> component. In a perfect world I would just use an anchor tag directly, but the Phoenix link has special properties.
But either way, am I correct in understanding that the shadow dom approach would prevent, say, colors from affecting a child component? If so, as I mentioned before that behavior is actually desired. I want those styles to leak, as it turns out to be quite useful.
I’m not sure what problems with vanilla CSS you’re referring to, so I can’t respond to this. I’ve outlined what I think the main problems with vanilla CSS are, and how both Tailwind and the scoping approach solve them.
The problem is that scoping selectors is actually very difficult and annoying to do manually. For example:
It is important here that you scope both of the class selectors in the second line of CSS. Like:
.foo[sh-XXXXXX] > .bar[sh-XXXXXX} { ... }
If you don’t scope every selector by default things will leak in weird unexpected ways. You actually have to properly parse and re-generate the selectors entirely, as it turns out. That’s why I need to rewrite my sloppy parser
You can imagine that this gets rather complicated for more complex selectors. Doing this manually would be super annoying and error-prone, I think. But it can be trivially automated, as I have shown!
If everything is scoped properly you gain a huge amount of freedom because styles in a component can never conflict with the rest of the codebase - it’s guaranteed.
You can do nested classes now, so it’s not as much of a problem:
.action-menu {
&[role=menu] {
/* ... */
}
}
But yes, in the (distant) past I would just scope everything. Not super fun but when each thing is in its own file, it’s easy to see when you go astray just visually but ya, certainly more error prone.
As for vanilla CSS, I think I may still not fully understand the depth of what your solution does, mostly in terms of cross browser finickiness.
Tailwind was based on a strict scale (--spacing-1, --spacing-2, …, --spacing-72), which were synonyms for rem, and they moved to calc on Tailwind v4 so you can use any value.
My biggest concern with rem is that it is tied to the font size and font sizes are not really standard between fonts, x-height, width, weight can all vary, which means you may change the font size of your app into something that now looks too small or too large, and then adjusting the font-size will change all of your app and the only fix is to revisit all rems. Having at least one var as reference (--spacing in Tailwind v4) gives you the ability to adjust that without changing your whole app.
The Tailwind vars seems to be a very useful baseline system that I would probably happily use with pure CSS but I do think that without something like custom functions, we end-up with an inferior spacing system, i.e. I’d love --space(1).
Yeah, it’s less painful than it used to be, but this still doesn’t buy you the same productivity as Tailwind. You still have to manually name the component, ensure its root has the given class, and then write the nested CSS in your “central” CSS file (no co-location). And you can’t have conflicting component class names, or things will break!
The nested CSS would, however, be a valid generator target for a library like mine. However, re-writing the CSS AST to the nested syntax would be a lot more annoying than injecting [sh-XXXXXX] into every selector, so it would be more work.
It extracts a <style> section from a heex component, mangles all of the selectors with [sh-XXXXX] (where the XXXXXX is a truncated md5 of the component’s name), and then injects the sh-XXXXX attribute into all of the HTML tags found directly within that component’s heex (using a rather degenerate regex, because I was too lazy to parse HTML). It then compiles the parsed CSS into a CSS file with all of the scoped selectors. This results in styles which are scoped to the heex templates in which they are defined.
It also has a bunch of “design system” stuff for managing variables, but that’s another thing.
Ya sorry I totally understand the scoping. I’ve used scoped CSS before that just made none-sensical classes out of hashes and it was terrible, yours is nicer. I just mean how does it deal vendor prefixes and whatnot. Although again, this is in relation to it being used for core components (even though it feels like we’re passed that now )
As for our thing, we have a generator which generates a component along with a CSS file so it’s pretty easy to find stuff and not forget nesting And of course we have a very well organized designer and everything has a name so conflicts have not been a problem. But ya, I’m getting kinda off track here
This is an interesting point. But what unit do you use instead? px? I just used rem because everyone else uses it
I could of course implement a set of spacing variables quite trivially (my variables are literally just kwlists, you can imagine a simple Enum.map(1..10, fn -> ... end) would take care of it).
But the key insight that led to me just using rem was that I am never going to change my spacing scale. Design work is extremely finicky, and I am not convinced that you can adjust spacing linearly without it breaking some parts and not others. I think in practice you would end up having to go through every component and adjust how it “looks” after changing the scale.
Now maybe if you defined each “level” if the scale by hand you would get there, but in turn I think you would end up always wanting more “in-between” levels ad infinitum (I already have this problem with colors).
I suppose a truly unhinged solution would be to define the scales as a curve…
Well personally, I just don’t (lol). But of course there is no reason you couldn’t run autoprefixer on the output - that’s your choice!
Actually, since I have to parse the entire CSS AST anyway, it would not be very hard to implement prefixing functionality in the library itself. I have considered doing that, at least for some basic (common) prefixes. But I really am fine just doing it by hand, as I rarely use prefixed features.