Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
mhevery opened this issue Jan 31, 2023 · 41 comments
Closed

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

mhevery opened this issue Jan 31, 2023 · 41 comments
Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation

Comments

@mhevery
Copy link

mhevery commented Jan 31, 2023

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

[ HackMD | #2767 ]

Goal

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

  • lazy-loadable: Load the styles on as needed basis.
  • type-safe: Developer should not worry that they have miss placed a style, or should feel that refactoring / renaming styles is hard.
  • tree-shakable: System should be able to remove unneeded styles.
  • Scalable: It should work on small as well as large projects. Should work with MFE composition.

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

Legend:

  • Inlinable: The developer should be able to place the styling inline with the markup. Why? Because creating a new file breaks the flow of the coding. Putting things in new file requires: Choosing a file name; Choosing a file location; Choosing the symbol name (name of class) which is a source of typos (see type-safe.); Moving styles between files does not feel safe (see type-safety.)
    I think this is part of the reason why CSS-in-JS, and tailwind are so popular, because they don't derail the developer flow. The developer styles the element and moves on without having to make any decisions.
  • Scopes Style: Large applications run into the risk of colliding style names. So a good solution should remove collision risk from the developers hands. There are two approaches: (1) mangle the style-names AND (2) append additional scoping selector and add the additional class to each element. We think that adding additional selectors to the DOM elements is sub-optimal because it creates issues such as: [✨] a way to declare component$ which shares parent component styling scope qwik#2726; Scope styling anchor elements rendered from <Link> qwik#2071; Allow "scoped" styles to be inherited by Qwik Components qwik#1710; and https://github.com/BuilderIO/qwik/discussions/1063.
  • Composable: It should be easy to reuse same styles across many different DOM elements/components. (Here I think tailwind is excluded because each element repeats all of the stylings over again.)
  • Type Safe: If developers are forced to create names for the classes, than these names should be type-safe, in the sense that a type (or refactoring) should tell the developer which code is incorrect. (Right now current CSS is not typesafe, which makes it append only kind of coding, as developers are afraid to remove selectors or rename them for a fear of breaking something.)
  • Needs Name (CON): Asking the developer to create a name should not be required. It is just extra work and decisions that they need to make. Things which are inlineable naturally do not need a name.
  • Scope DOM (CON): Scoping the DOM is suboptimal as we need to add the scoping information to each DOM element as we don't know ahead of time if it will be needed. Also the thing to scope should be the CSS not the DOM elements.

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:

  • redBorderFromString: devs can cut&paste from the dev-tools. Downside is that to get code completion they have to install an editor plugin.
  • redBorderFromObjLiteral: TypeScript can verify types, but cut&paste would not work from dev-tools/existing CSS.

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

  • Inlineable: The resulting syntax is inlineable into JSX markup. This allows the developer to naturally style the components without thinking how to, name the class, the file, or how to structure the files.
  • Type-safe: There is no risk that a class name is miss-spelled resulting in incorrect styling.
  • Scoped: Styles are automatically scoped, so there is no risk of leakage to other components.
  • Composable: multiple the CSS$ can be composed together or grouped into arrays and referred to by other JSX.
  • Lazy-loadable: The runtime is able to lazy load the styles without having the developer spending time to think about lazy loading.
@mhevery mhevery changed the title Atomic CSS / Inline of critical Styles / Inline CSS in JS which is lazy loaded # RFC: Lazy-loadable, type-safe, scalable style support Jan 31, 2023
@mhevery mhevery changed the title # RFC: Lazy-loadable, type-safe, scalable style support ✨ RFC: Lazy-loadable, type-safe, scalable style support Jan 31, 2023
@samijaber
Copy link

Will this be able to support:

  • media queries
  • pseudo selectors

@mhevery
Copy link
Author

mhevery commented Jan 31, 2023

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
Copy link

samijaber commented Feb 1, 2023

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

  • be introducing a brand new syntax to CSS
  • need to do this every single time for each property, which can be very cumbersome if you are describing a large set of hover styles for example
  • need a special editor plugin to parse and add syntax highlighting to the Qwik CSS syntax (whereas beforehand, you could likely re-purpose an existing CSS-IN-JS plugin that's out there with little work)

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'
}));

@samijaber samijaber reopened this Feb 1, 2023
@mhevery
Copy link
Author

mhevery commented Feb 1, 2023

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
Copy link

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
Copy link
Author

mhevery commented Feb 1, 2023

CSS$.hover.nthChild('2n+3')`color: blue`

@samijaber
Copy link

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
Copy link

DustinJSilk commented Feb 1, 2023

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
Copy link

ahnpnl commented Feb 1, 2023

I have 2 main things:

  • Will this also support CSS preprocessors?
  • Is it possible to have a way that developers can freely choose the class names instead of generated ones?

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

@zhuhaow
Copy link

zhuhaow commented Feb 1, 2023

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
Copy link

n8sabes commented Feb 1, 2023

[ 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
Copy link
Author

mhevery commented Feb 1, 2023

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
Copy link

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
Copy link

n8sabes commented Feb 2, 2023

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:

  • There are things you can achieve in pure css that are very, very difficult in JS objects, or not possible.
  • It's important that a style solution supports industry standard css syntax due to the large body of existing css from Style Artisans, and not force it to be ported to JS objects, which may not always be possible.
  • The style should be fully atomic and scoped (portable) at the component level.
  • There is also value in names, as discussed above (in pure syntax) so they may be composited or used by child components. This is highly desirable, enabling access to the parent by id-prefix (e.g. useId()??) as it relates to inheritance, but that is above my pay grade with Slots at the moment.

See Discord for @KenAKAFrosty's ChatGPT analysis.

@n8sabes
Copy link

n8sabes commented Feb 2, 2023

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
Copy link
Member

wmertens commented Feb 2, 2023

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
Copy link

n8sabes commented Feb 2, 2023

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
Copy link

KenAKAFrosty commented Feb 2, 2023

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.,:

  • Supporting both string literal syntax as well as type-supported object syntax? DX/ergonomics
  • Solution must support lazy loading: Performance

Some things may address both simultaneously which is great!

@manucorporat
Copy link

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

Scalability can be achieved anyway!

@manucorporat
Copy link

manucorporat commented Feb 3, 2023

My ideal solution looks like:

  • Inlined
  • Collocable inside the JSX (if you want)
  • Without need to come up with names
  • Refactorizable, common styles can be given name optionally and reused
  • Atomic styles
  • Global CSS
  • Build time generated, zero runtime

@n8sabes
Copy link

n8sabes commented Feb 4, 2023

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
Copy link

dzearing commented Feb 9, 2023

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

TLDR:

  • Ergonomics - styled atomic components are fun and ergonomic for non-reuse cases, better than inlining styles into components.
  • Reusability - consider how 3rd party devs can build component libraries in qwik which require reskinability. This might conflict with inlining.
  • Atomic css - may hurt more than help, given the design goals of Qwik.

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:

  • unclear why CSS$ is dollar signed. Is it a split point? Will that add more network hops?
  • Unclear on the importance of making selector syntax like :hover type-safe. If the css can be linted with css lint, that would probably catch things.
  • regarding bad deep selectors like a > li > foo, it's bad to do that. But also global.css is an antipattern. These kinds of things are usually overriding some style, which is a side effect of bad customizability (which is why we opted to publish components that are refactorable.) But also then sometimes you can't refactor the component; it's coming from some 3rd party package. How can we make re-styling safer in these situations without sacrificing type safety and scoping? I don't have a great answer here beyond some standardized DI system for components... which adds lots of complexity.

@n8sabes
Copy link

n8sabes commented Feb 9, 2023

@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
Copy link

dzearing commented Feb 13, 2023

@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:

  • How many .css files will end up being in the production asset folder? 1 per rule? Or are they "common-chunked"?
  • In production builds, when are they loaded - after the js has parsed and has requested a .css file, or are they kept in the .js and injected dynamically?

And, fwiw, my concerns:

  • rendering bulk (class names will be much longer, qwik leans heavy on server rendering which will now have these massive class lists per element)
  • download size/number of files (if you are extracting *.css files from these smaller *.js files, that's going to be a lot of common chunk *.css files)
  • developer debugging (visually looking at the DOM is now cluttered with massive hashed class lists, harder to understand why rules are getting added or where the corresponding source of the css is.)

@n8sabes
Copy link

n8sabes commented Feb 15, 2023

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
Copy link

n8sabes commented Feb 15, 2023

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

@wmertens
Copy link
Member

wmertens commented Apr 6, 2023

Interesting tidbits around atomic CSS:

CSS$:

  • @mhevery the CSS$ you propose, do I understand that the $ is required to make it auto-import the CSS?
    • But if the optimizer could instead support transforming css, it would have better editor support.
    • Can't the media queries etc be done by nesting instead of extra helper functions, just like vanilla-extract and others do it?
    • it would be nice to eval at build time like vanilla-extract does it
    • on the client it can use insertRule, but how will Qwik know to emit <style/> tags while streaming SSR?

styling libraries:

  • @dzearing I think libraries should accept class names that they apply where needed, that allow "styling" CSS.
  • Qwik should allow the libraries to provide their "structural" CSS, which is orthogonal to the "styling" CSS.
    • this means that the dev should be able to import the CSS to make it global or useStyle$ it to make it lazy.
    • importing .css files from node_modules doesn't trigger transforms I believe :-(

@dzearing
Copy link

dzearing commented May 30, 2023

@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
Copy link

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
Copy link
Contributor

shairez commented Jul 18, 2023

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
Copy link

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.

  • Currently I'm in the process of choosing the current ideal css framework/technology for Qwik in terms of SEO/CWV
  • Until your response, I was decided that I will use vanilla-extract (and sprinkles),
  • I see that pandacss is like 99% basically same? I tried finding some comparison but with no luck
  • So my last question is: Is there any notable difference between these 2 (vanilla-extract vs pandacss)? (I mean like, if pandacss has some more of the features, which were disscussed here for this RFC and it would make potential rewrite of this (pandacss) to future "native" QWIK solution (which is discussed here) easier?

@wmertens
Copy link
Member

wmertens commented Jul 19, 2023

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

Overview:

  • You can always do the classic importing of CSS, with modules if you like, inline if you like. No real type support (not sure if module.css files provide type hints)
  • TailwindCSS works for sure, it's atomic CSS but you won't have help with the classes
  • styled-components, emotion etc are not a good fit for Qwik due to the runtime requirements
  • Vanilla-extract works, and you can use my https://github.com/wmertens/styled-vanilla-extract if you want something similar to styled components
  • Panda-css is newer, but it does atomic styles and you don't need to have separate files for the styles. It also seems to have more development momentum.
  • I don't think there will be a "native" Qwik solution in the coming future or maybe ever. Qwik's philosophy is more about enabling all solutions with optimal performance, out of the box.

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
Copy link

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

@redbar0n
Copy link

@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
Copy link

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

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

@wmertens
Copy link
Member

Update:

  • UnoCSS works with qwik too
  • Compiled looks mighty interesting but they don't have a vite plugin nor qwik integration

@fabb
Copy link

fabb commented Oct 18, 2023

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
Copy link
Member

wmertens commented Oct 18, 2023

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

@fabb
Copy link

fabb commented Oct 18, 2023

@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: chakra-ui/panda#1109

@fabb
Copy link

fabb commented Oct 18, 2023

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
Copy link
Member

gioboa commented Oct 14, 2024

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

@github-project-automation github-project-automation bot moved this to In Progress (STAGE 2) in Qwik Evolution Oct 14, 2024
@github-actions github-actions bot added [STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation labels Oct 14, 2024
@gioboa gioboa transferred this issue from QwikDev/qwik Oct 14, 2024
@QwikDev QwikDev locked and limited conversation to collaborators Oct 14, 2024
@gioboa gioboa converted this issue into discussion #174 Oct 14, 2024
@github-project-automation github-project-automation bot moved this from In Progress (STAGE 2) to Released as Stable (STAGE 5) in Qwik Evolution Oct 14, 2024
@shairez shairez removed this from Qwik Evolution Oct 15, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation
Projects
None yet
Development

No branches or pull requests