QwikDev / qwik-evolution

Home for Qwik proposals and RFCs
15 stars 0 forks source link

✨ RFC: Lazy-loadable, type-safe, scalable style support #98

Closed mhevery closed 1 week ago

mhevery commented 1 year ago

RFC: Lazy-loadable, type-safe, scalable style support

[ HackMD | #2767 ]

Goal

Have an ergonomic way of styling components where the styles are:

Prior Art

A quick overview of existing styling solutions and their pros-cons.

Inlinable Scopes Style Composable Type Safe Needs Name Scope DOM
Pure CSS
Emotion
Vanila Extract
CSS Modules
useStyleScope$()
Tailwind
StyleX sheet

Legend:

Influence

There is an interesting article on Atomic CSS called StyleX This points out that as applications get very large the need for additional CSS reaches zero growth. This is because the styling is broken up into primitives which are then reused. We think this is a good approach for Qwik as well.

Proposal

import { component$, CSS$ } from "@builder.io/qwik";

export default component$(() => {
  return (
    <div class={CSS$`border: 1px solid green`}>
      <Greeter class={CSS$`color: red`} name="World" />
    </div>
  );
});

export const Greeter = component$<{ name: string; class: any }>((props) => {
  return <span class={props.class}>Hello {props.name}!</span>;
});

The basic idea is to create a CSS$ tagged string literal which can be used as:

const redBorderFromString = CSS$`
  border: 1px solid red; 
  border-radius: 50%
`;
const redBorderFromObjLiteral = CSS$({
  border: '1px solid red',
  border-radiues: '50%',
});

Both of the above examples are equally supported and are identical. Advantages between the two approaches are:

Pseudo Selectors and Media Queries

const hover = CSS$.hover`color: blue`
const hoverOL = CSS$.hover({ 'color': blue });

const smallScreen$ = MEDIA$`screen and (min-width: 480px)`;

const media = smallScreen$.hover`background-color: lightgreen`;
const mediaOL = smallScreen$.hover({backgroundColor: 'lightgreen';});

Return value

The return value of CSS$ is an opaque object which can contain 1 or more classes along with the associated QRLs. The returned values can be composed together in markup.

<div class={[redBorder,  blueText, etc...]}/>

Transformation

As you can see the CSS$ ends with $ which means it is subject to optimizer and lazy-loading.

import { component$, CSS$ } from "@builder.io/qwik";

const redBorder = CSS$`border: 1px solid red; border-radius: 50%`;

Will be transformed to:

import {QRL_PREFIX} from "@builder.io/qwik";
const redBorder = QRL_PREFIX + 'HASH_OF_JS#HASH_OF_STYLE1#HASH_OF_STYLE2';

file: HASH_OF_JS

export const HASH_OF_STYLE1="border:1px solid red";
export const HASH_OF_STYLE2="border-radius:50%";

NOTE: exact implementation to be determined and may be different.

Runtime & SSR

The Qwik runtime will be able to recognize the strings which are QRLs and will know to load a specific JS files and create <style> tags from those JS files.

The SSR will insert the <style> tags into the corresponding SSR output.

Because the styles are just JS loaded through QRLs, existing prefetching and bundling system will be able to optimize the loading of the styles.

The runtime can easily see which styles have already been loaded and which still need to be loaded.

No need for useStyle$()

With the CSS$ approach there is no need for useStyle$() to load the styles. The renderer is now intelligent enough to recognize when a QRL is being passed into the class and if it needs to be loaded. Because the rendering can delay flushing of the UI to DOM, the renderer can load the CSS without causing a flash of unstyled content.

Constraints

The CSS$ will be able to refer to static content only. So things like this will not be supported and will be a compiled error.:

const redBorder = CSS$`border: ${Math.rand()}px solid red`;

If the CSS needs to have variable, than CSS variables should be used.

Thoughts on CSS

CSS selectors can have complex rules such as body>ul>li. We think such rules are very hard to reason about and make the CSS append only as devs are worried that changing them will break something. Such rules are also hard to tree-shake for.

We think for styling components such rules are an anti-pattern and will not be supported by the CSS$ which has one-to-one connection.

Instead if you want to use such complex rules, global.css is a good place to put them, but you lose the ability to lazy load such rules.

Advantages

samijaber commented 1 year ago

Will this be able to support:

mhevery commented 1 year ago

Good call @samijaber updated the spec:

const hover = CSS$`color.hover: blue`
const hoverOL = CSS$({ 
  'color.hover': blue 
});

const smallScreen$ = MEDIA$`screen and (min-width: 480px)`;

const media = smallScreen$`background-color.hover: lightgreen`;
const mediaOL = smallScreen$({
  'backgroundColor.hover': 'lightgreen';
});
samijaber commented 1 year ago

MEDIA$ is interesting! I'll say that the <CSS_PROPERTY>.<PSEUDO_SELECTOR> format is confusing and unfamiliar. You'd:

What about, along the lines of MEDIA$, you include a PSEUDO$ in the spec?

const hover$ = PSEUDO$`hover`;
const hover = hover$`color: blue`
const hoverOL = hover$({ 
  color: 'blue'
});

const smallScreen$ = MEDIA$`screen and (min-width: 480px)`;

const media = smallScreen$(hover$`background-color: lightgreen`);
// not sure how you would combine 2 tagged templates, that part is a bit confusing to me 

const mediaOL = smallScreen$(hover$({
  'backgroundColor': 'lightgreen'
}));
mhevery commented 1 year ago

What about, along the lines of MEDIA$, you include a PSEUDO$ in the spec?

ohh! I like that!

What do you think of this:

const hover = CSS$.hover`color: blue`
const hoverOL = CSS$.hover({ 
  'color': blue 
});

const smallScreen$ = MEDIA$`screen and (min-width: 480px)`;

const media = smallScreen$.hover`background-color: lightgreen`;
const mediaOL = smallScreen$.hover({
  'backgroundColor': 'lightgreen';
});
samijaber commented 1 year ago

We need to account for pseudo-selectors that receive arguments... https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child

My suggestion allows arbitrary strings for pseudo selectors, but yours makes things messier. How would we do nthChild(2n + 3)? smallScreen$['nthChild(2n+3)']?

mhevery commented 1 year ago
CSS$.hover.nthChild('2n+3')`color: blue`
samijaber commented 1 year ago

I personally think adding typings to handle all of these could get...cumbersome to maintain. But up to you!

Also, as I think about this even more...would be cool to support attribute selectors, class selectors, and all sorts of other selectors. It makes me wonder if we could have a SELECTOR$ as a catch-all for users to be able to write whichever arbitrary selector, not just a pseudo one:

/* hover pseudo selector */
const hover$ = SELECTOR$`:hover`;

/* <a> elements with a title attribute */
const href$ = SELECTOR$`a[title]`;

/* All <li> elements with class="spacious" */
const spaciousLi$ = SELECTOR$`li.spacious`

But of course, the benefit of your approach is that it comes with types and autocomplete. Having both would be neat maybe?

DustinJSilk commented 1 year ago

This is cool! Something I really like about tailwind, having recently taken it up, is that you don’t use fixed values, you follow a theme and you extend it. It also comes with a great starting point. So it enforces some nice best practises

It feels like this could work with tailwind quite well. I wonder if there’s a way to integrate the 2, allowing us to import compiled CSS$ tailwind objects and use them to compose our styles as well. We can then stick with our UI frameworks and current theme and switching over would be a breeze.

I imagine vite could compile class names such as px-8 into an object in a cache called pX8. And these could be used like this:

import { px8, textWhite } from ‘virtual’

<div class={[px8, textWhite, etc...]}/>

I would see this as beneficial as the qwik-react integration which would make adoption easier and give new projects a leap forward.

is this something we could consider @mhevery ?

ahnpnl commented 1 year ago

I have 2 main things:

and a small thing is: can we have a concrete example with CSS variables?:)

zhuhaow commented 1 year ago

Will CSS$ be able to dynamically generate stuff at build time like what vanilla-extract does? vanilla-extract requires a dedicated .css.ts to do this.

In vanilla-extract we can do

// ****.css.ts

function generateStyle(config: LayoutConfig) {
    // style object is generated here.
}

export const styleA = generateStyle({...});
export const styleB = generateStyle({...});

// Even array
export const styles = [generateStyle({...}), generateStyle({...})];
n8sabes commented 1 year ago

[ Copying here from Discord ]

Good write up @mhevery. Thoughts:

  1. The css string must be able to support ALL aspects of the text within a css file in of itself to be fully functional and locally scoped. This includes keyframes and media queries to allow the css to be atomically complete. While I love emotion (endless hours with it and stylis)— This is a major deficiency of emotion that breaks atomically scoped styles by globalizing keyframes. It’s a design deficiency that requires special low-level work arounds via stylis to get true local component scope. We’ve had much dialog around this when @wmertens was writing styled VE.

  2. Pure css syntax is critical for easy porting as well as object format, which you’ve covered.

Below is a LONG thread (of 69 messages) worth scanning through containing conversation on the topic between several community members. There are many others messages out-of-band, repo Issues in VE, Emotion, and Stylis on the topic.

Atomic units of style | CSS as a first-class citizen for CSS-in-JS | Locally-scoped at runtime https://discord.com/channels/842438759945601056/842438761287254019/1044407650345046128

mhevery commented 1 year ago

I imagine vite could compile class names such as px-8 into an object in a cache called pX8. And these could be used like this:

import { px8, textWhite } from ‘virtual’

<div class={[px8, textWhite, etc...]}/>

I would see this as beneficial as the qwik-react integration which would make adoption easier and give new projects a leap forward.

i belive that could be implemented in the user land so it should just work.

Will CSS$ be able to dynamically generate stuff at build time like what vanilla-extract does? vanilla-extract requires a dedicated .css.ts to do this.

yes static code generation is the goal here

  • Will this also support CSS preprocessors?

Probably not.

  • Is it possible to have a way that developers can freely choose the class names instead of generated ones?

Right now no. What would be the purpose of that?

Also I will add some CSS vars examples

shiroinegai commented 1 year ago

As someone who was an active participant in that discussion that @n8sabes linked, here are some of my thoughts:

My initial impression was that this shouldn't be a priority and if it is, it seems like a lot of effort for not a lot of gain. This impression is a result of numerous Qwik talks demonstrating (and perhaps somewhat implying?) that optimising CSS does not lead to a huge performance improvement.

So I don't know for sure how this would work but if CSS$ streams styles as required, would there be an unintended side-effect of causing extra re-flow and re-paint (potential performance degradation if a lot of layout properties are used)? This could lead to "jumpy" layout shifts similar to how scrollbars can cause layouts to "jump" horizontally whenever they appear. Probably only an issue for those using Qwik primarily in an SPA manner.

I also have some concerns about perceived DX improvements.

As @n8sabes rightfully points out, being able to support all CSS properties via the string literal format is very much an expectation that CSS writers have. I believe this is quite important for adoption from those coming from existing CSS tooling like SASS/SCSS, Emotion, styled-components, CSS modules; and perhaps even Vue/Svelte(Kit) users who get to write co-located scoped vanilla CSS right at the component level.

And speaking of co-located scoped CSS, those two frameworks have them in easily collapsible <style> tags. On the other hand, CSS$ is either used inline within JSX or has to be expressed as a variable first.

Considering how divided people are on Tailwind's verbosity, I doubt people will be using CSS$ inline when it's guaranteed to be even more verbose than Tailwind given the exact same styling. So if we assume that train of thought, it means all of the variables will be declared at the top of the file or you'll just keep getting "variable is not defined" errors. It won't be a problem for simpler components, but for more complex ones, I don't think it would be appealing for dozens of

const styleName = CSS$`
  property: value
`

clogging up vertical real estate in their code editors, and worse, no easy way to collapse all of them together. Ironically, also causing everyone to think of names again.

So when that happens, I'm sure all the variables will end up living in their own separate file to be used as design tokens to be imported and composed. At this point, I'd argue that it ends up being a worse version of vanilla-extract since it will require the user to learn additional syntax for CSS queries while not supporting complex selectors.

The current spec also has maintenance problems. CSS has been on a trend of introducing new features such as :is, :has and :where. You'd have to be constantly aware of such updates and implement them to fit the current spec if such features gain popularity or face churn as people just return to writing more of regular CSS as it grows more powerful. CSS custom properties changed the game so much that even SASS received a dip in popularity, and with CSS getting built-in nesting in the future? I'm not sure if it spells well for a spec that introduces a new syntax.

There's already excellent CSS tooling or frameworks that help tame the CSS beast. Tailwind proudly boasts that most projects end up using less than ~10kb of CSS in their docs and while I do see JS bundle sizes trending upwards, I can't say the same for CSS when looking at Tailwind's growing popularity. Also, to add to the examples already provided, I recently chanced upon another one that could be interesting to watch: https://css.master.co/

I just want to say that I sincerely believe Qwik is pushing the web forward with some truly novel innovation, in fact, pushed it so hard that a once lurking hobbyist like me got so excited about the future of web dev that he's now actively participating in contributing to open source over at Qwik UI. If anything, my opinion is largely observation as I don't have professional experience in the industry but I hope I provided some useful feedback!

n8sabes commented 1 year ago

Thank you @KenAKAFrosty for providing the ChatGPT analysis. Below are excerpts of the summary for those whom do not wish to read the extensive dialog in just one of the threads on this topic —

  1. There is value in both a TypeScript-based CSS system as well as existing body of work and knowledge around CSS (style.css files).
  2. There are designers who are proficient with CSS stylesheets but not TypeScript.
  3. Auto-completion is possible with both syntaxes and is high-value.
  4. There are times for style text/files AND JS Objects -- both should be fully supported.
  5. It is important to avoid a partial implementation of a CSS syntax, such as what Emotion did, as a new de facto standard.
  6. For the product in this conversation, the ability to import simple CSS snippets makes sense.
  7. Keyframes are an important part of a stylesheet and need to be treated as such.
  8. Vanilla-Extract already supports @media queries but needs @keyframes support.
  9. Vanilla-Extract is not using stylis for their library, but it needs to be used for this functionality.
  10. Media queries are already done and @keyframes should be supported by VE like @media is.
  11. Going from VE syntax to CSS is hard, going from full .css to .css.ts is also hard.
  12. wmertens has agreed that styled should support @keyframes.
  13. wmertens added keyframe support in styled so the solution is now fully atomic and scoped to the component, closing the loop.
  14. wmertens filed an Issue with VE to include keyframes, supported by n8sabes.

The analysis covered many high-level points discussed in the thread, but much is out of band in VE, Emotion, or Stylis Issues. A few points:

See Discord for @KenAKAFrosty's ChatGPT analysis.

n8sabes commented 1 year ago

Here is an index.tsx file to play with that demonstrates compiling / tokenizing a whole style and its dependencies as an atomic unit (file / text block). As you can see, it's easy to walk the compiled Abstract Structure and set component-scope as a WHOLE ATOMIC unit.

I was looking back at my old code from 2020 that scopes everything, but it's highly customized for @emotion + stylis. There are some minor gotchas in the animation shortcut property vs. animation properties broken out into individual attributes, but very easy and doable.

Example 2 in the code below is just an idea for a default style using floating un-classed style properties in the css-file/text-block. To use this, 1) After scoping, separate the floating properties and send them to the tag, 2) then inject an inline style ahead of the tag for all the rest (named classes, id, keyframes, media queries, etc.). This way children can use the scoped classes by name / id albeit with a little useId() alchemy helper functions.

NOTE: I believe I used emotion js-object structures and its serializeStyles helpers for serializing js-objects, but there may be other / better alternatives for js-object structures 🤔. All CSS-in-JS libraries seem to have complicated structures that are not intuitive when translating elaborate (pure) css to js-objects, thus the need to support pure old-school css. I believe you even state this challenge in your original Issue post, suggesting they may not be supported.

import { $, component$, useId } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
import { compile, serialize, stringify } from 'stylis'

export default component$(() => {
    const componentId = useId();

    const experiment1 = $(() => {
        // debugger;
        const cssString = `@keyframes fadeIn { 0% {opacity:0;} 100% {opacity:1;} } .oBox{ opacity: 1; box-sizing: border-box; animation: fadeIn 5s;}`;
        const compiled = compile(cssString)
        console.log(compiled);
        const serialized = serialize(compiled, stringify)
        console.log(serialized);
    });

    const experiment2 = $(() => {
        // debugger;
        // 🤔 Maybe floating styles not contained within a class could be used as a default for style
        const cssString = `@keyframes fadeIn { 0% {opacity:0;} 100% {opacity:1;} } .oBox{ color: red;} opacity: 1; box-sizing: border-box; animation: fadeIn 5s;`;
        const compiled = compile(cssString)
        console.log(compiled);
        const serialized = serialize(compiled, stringify)
        console.log(serialized);
    });

    return <>
        <div>Stylis Experiments -</div>
        <div>
            <button onClick$={() => { experiment1() }}>Experiment 1</button>
        </div>
        <div>
            <button onClick$={() => { experiment2() }}>Experiment 2</button>
        </div>
    </>
});

export const head: DocumentHead = {
    title: 'Syle Experiments',
    meta: [
        { name: 'description', content: "Syle Experiments" },
    ],
};
wmertens commented 1 year ago

Does anybody know in general how CSS manipulation influences browser resource use?

For example, if you do a class per single CSS attribute+value, you end up with a very long classList on every element. How does the browser cope with this?

Another thing I'm wondering about is, how happy is the browser when you add/remove new styles incrementally. Does every rule add mean work, so it's better to do it in batches? Or does it only matter when the rules are used?

Any articles about this welcome.

BTW, I know that FB uses XStyle and I checked it out in the browser, looks like they have a bug where they produce the same CSS rules in multiple style blocks. Something to watch out for then ;-) Screenshot from 2023-02-02 13-45-19

n8sabes commented 1 year ago

I don't think a solution is too far afield from where we are now with the core. While working with this a couple months ago, I remember the hash-prefix scoping doing a good job on the css file. If not, the stylis approach shared above works great.

The question is, who has the best css js-object schema used for Css-in-JS to learn from (or adopt). I liked Emotion best, which may have evolved, or there may be better object schemas today I'm unaware of.

BTW -- Here is an example of some complex css that I cannot even think of how to achieve in a JS-Object, thus the need to support pure css syntax:

/* @n8sabes; Inspired by https://codepen.io/tonycorp/pen/JRLaKw */

.float-label {
  display: block;
  position: relative;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  padding: 2px;
}

/*
  Label when reduced (this is the default that will be modified, when not focused)
*/
.float-label label, .float-label>span {
  position: absolute;
  top: 5px;
  left: 10px;
  cursor: text;
  font-size: 1em;
  opacity: 1;
  transition: all 0.2s;
  user-select: none;
  color: rgba(0, 0, 0, 1);
}

.float-label>span[data-invalid] {
  color: red;
}

.float-label input {
  font-family: 'Courier New', Courier, monospace;
  font-size: 1.75em;
  padding: 0.8em 4px 0.5em 10px;

  /* Underline Style */
  /* border: 0;
  border-radius: 0;
  border-bottom: 2px solid rgba(0, 0, 0, .1); */

  /* Box Style */
  box-sizing: border-box;
  border-radius: 8px;
  border: 2px solid rgba(0, 0, 0, 0.5);
}

.float-label input::placeholder {
  opacity: 1;
  transition: all 0.2s;
}

.float-label input:placeholder-shown:not(:focus)::placeholder {
  opacity: 0;
}

/* Full Size Label, this modifies the small size */
.float-label input:placeholder-shown:not(:focus)+* {
  font-family: 'Courier New', Courier, monospace;
  font-size: 1.75em;
  left: 2px;
  top: 2px;
  padding: 0.8em 4px 0.5em 10px;
}

.float-label input:focus {
  outline: none;
  border-color: rgba(0, 0, 0, .5);
}

.float-label input[aria-invalid=true] {
  border: 2px solid rgba(255, 0, 0, 1);
}

.float-label input[nospinbuttons]::-webkit-outer-spin-button, .float-label input[nospinbuttons]::-webkit-inner-spin-button {
  margin: 0; /* Fix margin even though it's hidden */
  -webkit-appearance: none;
  -moz-appearance:textfield; /* Firefox */
}
KenAKAFrosty commented 1 year ago

I'd like to respond to a few things in here when I have more time, but I thought it very imperative to stop and point something out.

We should stay very diligent about splitting the conversation between DX/ergonomics, and performance

e.g.,:

Some things may address both simultaneously which is great!

manucorporat commented 1 year ago

Something to think about is lazy loading might not be that important with atomic styles! CHdIhfe

Scalability can be achieved anyway!

manucorporat commented 1 year ago

My ideal solution looks like:

n8sabes commented 1 year ago

Qwik Style Lab

Pease watch the two videos for the Qwik Style Lab I built for this topic.

Videos

Video 1 - Namespace and CSS$ default styles

Video 2 - useStyleScopeId()

Repo and StackBlitz

Qwik Style Lab Repo

Demo on StackBlitz

NOTE: All styles that are the same will have a hash to prevent duplication (atomic / one instance). Variables should be css variables to make the styles dynamic.

dzearing commented 1 year ago

Just my opinions fwiw - notes on the RFC after thinking about it for a bit.

TLDR:

Ergo notes

Not sure inlining styles is something I would choose but one size doesn't fit all. I do like atomic styled components, feels like a good balance between readable code and minimal cognitive load.

Consider this example:

const CardBackground = styled.div` ...rules... `;
const CardTitle = styled.h1` ...rules... `;

const Card = function(props) {

  return (
    <CardBackground>
       { title && <CardTitle>{title}</CardTitle> }
    </CardBackground>
  );
};

Why I prefer atomic components over inline styling or styling functions;

The Card is just logic. It's easy to read, no styles. The atomic components CardBackground and CardTitle are style only and create building blocks. We do this in traditional css/js component development anyways. This skips a bunch of the cognitive load the RFC calls out without sacrificing the readability of the primary stateful component.

Reusability

When we designed the Fluent UI v9 component library, one big issue we encountered was delivering components which could be easily reskinned in a type-safe manner, without "overriding" the existing styling. Having the styles inline would strongly tie styles to components - even imports effectively hard-code the styling dependency.

We ended up with a model where each component could be imported as an out-of-box component, or you could recompose it by importing the style hook, state hook, and render function separately and using or replacing any of them.

A style hook would look like:

https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-button/src/components/Button/useButtonStyles.ts

...which looks a lot like some of the examples above.

import { makeStyles } from 'griffel';

export const useButtonClasses = makeStyles({
  root: {
    ':active': { color: 'pink' },
    ':hover': { color: 'blue' },
    // 💡 :link, :focus, etc. are also supported

    ':nth-child(2n)': { backgroundColor: '#fafafa' },
  },
  etc.
});

function Button = (props) => {
  const classes = useButtonClasses();
  const state = useButtonState(props, classes);

  return renderButton(state);
}

This lets the devs reuse parts and refactor new component variations and extend or replace the style parts. It certainly has more cognitive load than whipping out some atomic components. This is valuable for shared components that need reskinning, but less so for basic websites that don't need reusability.

This is just something on my mind when I look at Qwik's styling approach, and thinking about how something like this could be achieved. I think I tried this with useScopedStyles$ and ran into issues.

Atomic css concerns

First, I want to call out @layershifter who pointed me to his notes on atomic css used by Griffel and the tradeoffs encountered:

https://griffel.js.org/react/guides/atomic-css

There are some considerations in those notes to call out: in particular, the costs of recalc-ing class names and the overhead they incur is something to pay attention to.

The whole goal of atomic css is to reduce total class definitions. However, this requires that the app is sufficiently large to reach an inflection point. At the start, the atomic approach will result in far more class name definitions and chunkier rendering costs. At an inflection point, you will see less classes overall (assuming the stylesheets are extracted/deduped across the full app graph.)

Compare a stream of html and css with 1 rule that has 3 properties, vs the same with atomic:

.abc123 { background: red; color: black; font-size: 1em }
<html><div class="abc123">Hello</div></html>

Same but with atomic:

.abc123 { background: red; }.abc124 { color: black; }.abc125 { font-size: 1em }
<html><div class="abc123 abc124 abc125">Hello</div></html>

It's pretty clear with this simple example that atomic is going to end up being far more expensive when the stylesheet is trivial, but at some inflection point, if enough components re-use display: block the theory is that it will result in less insertRule calls. A logarithmic curve.

When I think about the design goals of Qwik, the premise is to load only the content you need, and ideally render as much on the server as possible and resume only what's needed. That is, it is an anti-goal to ever reach that inflection point where atomic starts seeing gains. The absolutely most efficient experience will be one which loads the least amount of html, css, and js to achieve the desired experience.

Other rough notes:

n8sabes commented 1 year ago

@dzearing, Good points and appreciate the depth of your thought process on this. If I understand you correctly, I 100% agree a styled component is a fundamental feature that needs to be supported. @wmertens wrote a styled component for vanilla-extract which provides the ability for the CardBackground & CardTitle component example.

Expanding upon this, the styled component should accept both standard css and js-in-css object formats, and include all things required to make a whole unit of style (e.g. including keyframes, etc.).

The atomic aspect (using hashes to store exactly one instance of a whole style) is of great value, and the next question is how to also enable a locally-scoped style stack the ability to walk up contexts for css-based template / component libraries.

I likely need to re-read what you wrote another time or two to fully digest what you posted.

dzearing commented 1 year ago

@n8sabes Sorry for the long winded post; great to hear that styled is in the works. It really reduces cognitive load.

The atomic aspect (using hashes to store exactly one instance of a whole style) is of great value, and the next question is how to also enable a locally-scoped style stack the ability to walk up contexts for css-based template / component libraries.

Probably the biggest point from above: regarding using atomic css (1 class = a predictable hashed selector class + 1 name/value pair):

What value are we expecting to get, and will it end up being worth the tradeoffs?

We are rolling out atomic css usage at Microsoft, and we are hitting issues. It is worth exploring if Qwik plans on going down the same path, but I'm not sure if that is the case. There's a lot in the RFC and I might have missed some critical point so if I'm understanding the proposal correctly, here's what I got:

The css is split into small js modules imported by the source. This means that the css rules can be tree-shaken and included with the split points generated by the optimizer. Then later, there's an extraction step which pulls the css out of the .js files and into .css files, so that the rules can be loaded in parallel to scripts.

If that's right, here are my questions:

And, fwiw, my concerns:

n8sabes commented 1 year ago

I'd like to inject the topic of Semantic CSS patterns that leverage ARIA properties into this conversation. It's really an interesting and powerful means of creating near fully-functional components with no (or very little) javascript. Here is a css rule example:

[role="tab" ][aria-selected-"true"] {
    background: var(--clr-neutral-100);
    color: var (--clr-neutral-900);
}

Kevin Powell does a great job of explaining it, albeit he's more of a CSS Wizard than a JS coder: How to write Semantic CSS

n8sabes commented 1 year ago

@dzearing, You've brought up some great points and questions. A conversation would be of value on this topic.

wmertens commented 1 year ago

Interesting tidbits around atomic CSS:

CSS$:

styling libraries:

dzearing commented 1 year ago

@dzearing I think libraries should accept class names that they apply where needed, that allow "styling" CSS.

@wmertens 100% agree, devs need an escape hatch. You're also right about single character class names causing clashes with multiple independent projects on the page. Gotta use hashing to keep the class representations predictable across independent sources in order to support scenarios akin to module federation or pre-bundled artifacts. Hadn't thought about using unicode in the classname, interesting but sounds painful to debug.

Roman-Simik commented 1 year ago

Hi, I'm really sorry for interruption, but my comment won't add nothing new to this discussion. But I really like this idea, which would be potentially the best solution for handling CSS in terms of performance, CWV etc...

My question is: Is there is any ETA when this feature would be done? I mean, are we talking in months, year, 2 years? And possibly what can we do to push this feature forward in roadmap priorities?

Thanks !

shairez commented 1 year ago

Hey @Roman-Simik fair question

Recently we've been looking a lot into Panda CSS, @manucorporat even created a vite plugin to make sure we can integrate with it.

From all of the solutions, it looks like the one who provides the combination of all the benefits we discussed. So it's still in research but you can start playing with that by following this guide: https://qwik.builder.io/docs/integrations/panda-css/#panda-css

I imagine we'll have something more ready in terms of usage and examples in the near future

Roman-Simik commented 1 year ago

Hey @Roman-Simik fair question

Recently we've been looking a lot into Panda CSS, @manucorporat even created a vite plugin to make sure we can integrate with it.

From all of the solutions, it looks like the one who provides the combination of all the benefits we discussed. So it's still in research but you can start playing with that by following this guide: https://qwik.builder.io/docs/integrations/panda-css/#panda-css

I imagine we'll have something more ready in terms of usage and examples in the near future

Hi, thanks for the fast response and suggestion.

wmertens commented 1 year ago

@Roman-Simik the CSS landscape for Qwik is still settling down.

Overview:

So personally, I'd use Panda, unless I would be on a tight deadline with no time to experiment. In that case I'd pick Tailwind. I'm now more inclined towards UnoCSS for its speed and simplicity, although https://github.com/atlassian-labs/compiled looks extremely interesting too.

At a higher level, there's no full component libraries for Qwik yet (that I know of) so you would either make your own or use a JS-less UI like daisyui. https://qwikui.com/ is still under heavy development.

Roman-Simik commented 1 year ago

Hi, thanks a lot for the fast responses, really helpfull info !

redbar0n commented 1 year ago

@mhevery The link to StyleX in your original post should probably go to the official Facebook/Meta StyleX library (yet unreleased, but they want feedback on their docs site), since I presume you meant Facebook's Atomic CSS called StyleX, which you also linked to.

brandonpittman commented 1 year ago

If you're using Vanilla Extract, I ported some helpers I used with React.

https://github.com/brandonpittman/qwik-dessert

wmertens commented 1 year ago

Update:

fabb commented 1 year ago
const redBorderFromString = CSS$`
  border: 1px solid red; 
  border-radius: 50%
`;
const redBorderFromObjLiteral = CSS$({
  border: '1px solid red',
  border-radiues: '50%',
});

Both of the above examples are equally supported and are identical.

There is a small use case that is only supported by redBorderFromString: fallback values for the same property:

height: calc(100vh - 100px);
height: calc(100dvh - 100px);

Browsers that do not understand the value (the dwh unit in this example) will pick the last declaration they understand.

wmertens commented 1 year ago

@fabb I believe most css convertors support passing an array for multiple values of the same prop

fabb commented 1 year ago

@fabb I believe most css convertors support passing an array for multiple values of the same prop

In panda css not, because there array syntax is already used for responsive values: https://github.com/chakra-ui/panda/discussions/1109

fabb commented 1 year ago

Constraints

The CSS$ will be able to refer to static content only. So things like this will not be supported and will be a compiled error.:

const redBorder = CSS$`border: ${Math.rand()}px solid red`;

If the CSS needs to have variable, than CSS variables should be used.

Does this mean css cannot be dependent on props? Or do we just need to use conditionals to pick between different css depending on a prop?

e.g.

const redBorder = CSS$`border: 1px solid red`;
const greenBorder = CSS$`border: 1px solid green`;

export const Greeter = component$<{ error: boolean }>((props) => {
  return <span class={props.error ? redBorder : greenBorder}>Hello</span>;
});
gioboa commented 1 week ago

We moved this issue to qwik-evolution repo to create a RFC discussion for this. Here is our Qwik RFC process thanks.