Skip to content
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

Support scoped CSS in the same file #5224

Open
gaearon opened this issue Oct 2, 2018 · 87 comments
Open

Support scoped CSS in the same file #5224

gaearon opened this issue Oct 2, 2018 · 87 comments

Comments

@gaearon
Copy link
Contributor

gaearon commented Oct 2, 2018

This is kinda vague but I'd like to have a built-in option to write CSS that:

  • Exists in the same file as my components
  • Is scoped
  • Has auto generated class names, like CSS Modules
  • Extracted via the same pipeline as CSS Modules
  • Could potentially use Sass (since we already support it)
  • Fully static

Basically I want "CSS Modules" but without the "Modules" part. Just put it in the same file if that's the only place I use it anyway.

https://github.com/4Catalyzer/astroturf looks related. cc @jquense @markdalgleish

@fang0rnz
Copy link

fang0rnz commented Oct 2, 2018

Would this be like opening a style tag in .vue single file components? I think it'd be a great addition

@gaearon
Copy link
Contributor Author

gaearon commented Oct 2, 2018

Also relevant: https://github.com/callstack/linaria

Would this be like opening a style tag in .vue single file components?

Yeah. Except I don't want to partition file into sections like Vue does. Keep it JS but add the ability to add CSS styles inside.

@Timer
Copy link
Contributor

Timer commented Oct 2, 2018

This sounds a bit like styled-jsx.

@jquense
Copy link
Contributor

jquense commented Oct 2, 2018

TBH I think astroturf covers all these points, at it's core it's co-locating CSS files in JS. It's actually implemented as that; the tag content is extracted to a CSS file (not written to disk via webpack magic). The really nice thing about this approach is you get ALL the benefits of writing separate files without the work. For instance mini-extraxt-css-plugin and html-plugin both automatically works without any additional setup.

We've got all the relevant standalone loaders, plugins and Babel plugins in repo, you could integrate with a global css tag with all the library details hidden.

We've been using this for all our large projects with Sass and its really hit a nice sweet spot for convenient component API and playing nice with the tons of existing CSS tooling

@jquense
Copy link
Contributor

jquense commented Oct 2, 2018

I will say css-modules can be wonky and weird but if you're using them, I think the inline approach is really an ergonomic improvement. Cc @ai too

@threepointone
Copy link

an astroturf macro would be nice to start playing with this in CRA right now. it appears astroturf has a babel plugin already, so making a macro version shouldn't be too hard?

@kdekooter
Copy link

It would be great to treat CSS as a first-class citizen in ReactJS!

@ai
Copy link
Contributor

ai commented Oct 2, 2018

linaria is a nice project, but right now I prefer astroturf, because of more high order API:

  1. styled to created React component like in styled-component.
  2. Syntax sugar for modifiers: &.primary then can be used as <Button primary>.
  3. Syntax sugar for keyframes. You can write them in the same styled declaration and they still will be scoped.

@renatoagds
Copy link
Contributor

@jquense just a question, looking at astroturf repo I didn't find that. That's an way to create a local class, like :local from css-modules does?

@jquense
Copy link
Contributor

jquense commented Oct 2, 2018

@threepointone

The macro should be easy enough since the core is a babel-plugin, tho i'm not sure how'd deal we the extracted css files, they'd need to be written to disk somewhere. In a webpack env tho the loader avoids this by using a virtual filesystem plugin, exposes the files only to webpack's fs

@renatoagds astroturf is css-modules, so the behavior is the same, we have :local by default on since that's what css-loader does by default, so all classes are scoped. you can use :global to create global ones instead

@taion
Copy link

taion commented Oct 2, 2018

Yup – so the contrast here is that astroturf just handles pulling out the CSS into a separate virtual file, without imposing any of its own opinions on how to do CSS processing. It fully uses the existing CSS processing pipeline, which means that if you have Sass set up, then it will run Sass on your styling code – and this is exactly how we use it.

@renatoagds
Copy link
Contributor

Do we have anyone that use astroturf in production ? To share the pros and cons using it.

@taion
Copy link

taion commented Oct 2, 2018

We use astroturf in production right now.

For the part of the API as described above, there's not really any "experience". The whole point is that it doesn't do anything at runtime – it just pulls the CSS out into a separate file, and lets the rest of your pipeline take over.

The experience analogous to defining a CSS module next to my JavaScript, just without having to go bother with having a separate file.

@ai
Copy link
Contributor

ai commented Oct 2, 2018

@renatoagds I am using it in production. I use both css and styled function in big React. I converted the project from Emotion.js to astroturf in a few days (mostly because of our auto-import hacks). Animation, many custom PostCSS plugins, webpack CSS minification — everything work. Compare to Emotion we even fix few issues and reduce code size (thanks to &.modifier and @keyframes syntax sugar).

The only problem is that Stylelint doesn’t support it right now. But I created an issue and fix is very simple.

@satya164
Copy link
Contributor

satya164 commented Oct 2, 2018

Linaria covers all of the points,

  • Exists in the same file as my components - ✅
  • Is scoped - ✅
  • Has auto generated class names, like CSS Modules - ✅
  • Extracted via the same pipeline as CSS Modules - ✅ it doesn't use CSS modules, but it integrates with the same pipeline as rest of the CSS files
  • Could potentially use Sass - ✅ it's not necessary since you can use JS for logic, but possible
  • Fully static - ✅ the generated CSS is fully static, with support for dynamic prop based styles via CSS variables.

@ai Linaria supports similar features, has higher order styled API with CSS variables integration and syntax sugar for scoped keyframes. It doesn't support syntax sugar modifiers because we prefer it to keep as close to vanilla CSS as possible and these modifiers can be implemented in userland with a HOC.

In addition to these, Linaria has:

  • Proper specificity for styles when using styled (needs evaluate: true)
  • Fully working stylelint pre-processor
  • Sourcemap support the CSS, you can go to the place in JS file which created the CSS from devtools
  • Syntax Highlighting and autocompletion support in editors (e.g. - styled-components plugin in vs code)
  • Written with static analysis tools in mind, e.g. eslint can catch unused classes, works great with TypeScript and Flow etc.

@satya164
Copy link
Contributor

satya164 commented Oct 2, 2018

Regarding the Babel plugin writing files to disk, we used to do that with Linaria but ran into all sorts of problems. For example, if the Babel plugin is co-located with other Babel plugins, you need to have different configs for server, testing, linting etc. to not write files to disk. And if the Babel plugin is just used inside the webpack loader, then there no reason for it to write CSS to disk anyways.

@ai
Copy link
Contributor

ai commented Oct 2, 2018

@satya164 yeap, I like all types of zero runtime CSS-in-JS =^_^=

@giuseppeg
Copy link

giuseppeg commented Oct 3, 2018

I wonder if it is possible to use the current toolchain and write a small babel macro to compile styles with postcss-modules and rewrite the declaration to be compatible with the mini-css-extract-plugin format that in a nutshell is like this https://github.com/giuseppeg/dss/blob/1f183805e790e1721242255cc7d03cfff56d20bd/webpack/loader.js#L19-L24

export.locals is your { class: hashedClass } map. The fact that mini-css-extract-plugin expects the module export to be an array could be problematic though. Maybe @bebraw and @ai can help with validating the idea.

Usage would be like this

import cssModule from 'css-modules/macro'

export default () => <div className={styles.root} />

const styles = cssModules`
  .root { display: block }
`

@ai
Copy link
Contributor

ai commented Oct 3, 2018

@giuseppeg it is completely what astroturf does 😄 Just a CSS Modules (postcss-modules is the same CSS Modules but for webpack) for Babel with few syntax sugar.

@giuseppeg
Copy link

hah cool then! FWIW all the solutions suggested so far (css modules included) have a problem in common that is the cascade. When extracted the css is concatenated in some order so if you import some styles from another module the result is not necessarily predictable.

@satya164
Copy link
Contributor

satya164 commented Oct 3, 2018

@giuseppeg in Linaria, if you use the styled helper instead of passing class names around, it'll ensure that a higher specificity is applied when needed and the result is predictable.

@giuseppeg
Copy link

@satya164 cool I guess that with the styled-components like approach it is doable (assuming you are not allowing composition).

@everdimension
Copy link
Contributor

@gaearon

Basically I want "CSS Modules" but without the "Modules" part

Could you please elaborate on that? "Modules" mean that the class names get "scoped", which is what you mention as a desired feature.

I also want to mention that it's great what you're doing here. I fully support you endeavor to implement a fully static css solution!

@gaearon
Copy link
Contributor Author

gaearon commented Oct 3, 2018

I just mean that I don't want two files.

@everdimension
Copy link
Contributor

Oh ok. I can relate to that (kinda 🙂)


I actually wanted to mention something else.

In my opinion, a perfect solution would not only be zero-runtime, but it would also not have any cute syntax shortcuts. E.g. things like this

const styles = css`
  .button {
    color: black;

    &.primary {
      color: blue;
    }
  }
`

seem "nice", but it's not valid css. What if I wanted to use real css nesting from the CSS specification? Granted, nesting is not often needed with "scoped components" model, but let's discuss it for the sake of argument.

It seems to me that I would not be able to do that because it would conflict with the custom syntax of the library. But if the library only supported bare css, then adding nesting would be as easy as adding a postcss plugin. And it would be very similar to how we use babel plugins to enable js features from the spec.

Same goes for other css features as well.

Writing css in a javascript file might feel awkward for those who are not used it and I believe it's important to be able to say the them "well, you can write any valid css here and it will work". That's how CSS modules work. And just being able to have computed values with js template strings is a great addition which is quite easy to grasp.

@ai
Copy link
Contributor

ai commented Oct 3, 2018

@everdimension yeap, astroturf doesn’t have CSS syntax extensions. It is just PostCSS plugins

@taion
Copy link

taion commented Oct 3, 2018

You technically don't even need to pull in the PostCSS bits for astroturf – it's sufficient to just pull in astroturf/loader and set up css-loader normally, though you probably want to enable modules for things to work the way you want.

I think the delta we want here on astroturf would be something like:

  • Extract things with in a css template literal to a Foo.module.css file (instead of just Foo.css)
  • Extract things with in a sass template literal (or should it be scss?) template string to a Foo.module.scss file

@giuseppeg
Copy link

giuseppeg commented Oct 3, 2018

@taion not sure if you can generate and enqueue files in webpack from a loader (babel-loader or astroturf/loader in this case). You'd have to run the pipeline again. If I am right, the only option is to rewrite the file with babel so that a plugin can extract the compiled (scoped) styles to file.

@taion
Copy link

taion commented Oct 3, 2018

@giuseppeg You can do that – it's exactly what astroturf does.

@jquense
Copy link
Contributor

jquense commented Jul 2, 2019

@satya164 we should get together and build some sort of ultimate runtime free amalgam of astroturf and linaria :P

@satya164
Copy link
Contributor

satya164 commented Jul 2, 2019

@jquense haha why not, I just need to find time :D

@jednano
Copy link

jednano commented Jul 2, 2019

Is there something missing from Linaria that Astroturf has? Perhaps a side by side feature comparison would be useful in this thread?

@nhooyr
Copy link

nhooyr commented Jul 3, 2019

@ai it's only 16kb gzipped.

@ai
Copy link
Contributor

ai commented Jul 3, 2019

We should care about JS size not because of download time, but because of the time for processing and execution. On slow phones this time could be bigger than download time. And execution time can't be cached as we can cache file from downloading.

Here is more theory about cost of JavaScript:
https://v8.dev/blog/cost-of-javascript-2019

Since we are talking about processing time, minified size is more representative. Gzip size can hide some important repeats which we need to parse and execute.

@nhooyr
Copy link

nhooyr commented Jul 3, 2019

We should care about JS size not because of download time, but because of the time for processing and execution. On slow phones this time could be bigger than download time. And execution time can't be cached as we can cache file from downloading.

I agree it's important but I think in this case the gains outweigh the costs. react-dom itself is 100kb minified along with react at 6.5kb but still very widely used as it makes development much easier.

Likewise, I'm arguing that dynamic css in js is worth the cost as well as it makes it much easier to theme your app, use props to control styles and your code reads much better.

Linaria is clever in that it lets you do pretty much the same for most use cases with css variables but it won't work on IE 11 which is very unfortunate. And astroturf won't let users dynamically adjust css at all.

We can also use emotion which is pretty much identical and only 27kb minified. [https://bundlephobia.com/result?p=@emotion/core@10.0.14) and @emotion/styled.

@ai
Copy link
Contributor

ai commented Jul 3, 2019

Size is not the only problem. Parsing CSS string on any props changes and re-render the whole page on any dynamic styles changes is also more important.

I think that these 3 problems (size, render, repaint) is a very big cost for lack of user-defined styles in IE (you still can use dynamic styles in IE without Custom Properties with predefined options) and lack of themes in IE.

@nhooyr
Copy link

nhooyr commented Jul 3, 2019

Size is not the only problem. Parsing CSS string on any props changes and re-render the whole page on any dynamic styles changes is also more important.

Are you sure about that? Seems like a massive bug. I can't seem to find anything on github about it except for this old issue: styled-components/styled-components#356

I agree dynamic css in js is not the move if this is a real, reproducible issue.

@ai
Copy link
Contributor

ai commented Jul 3, 2019

@nhooyr SC uses template string and put props inside this string. So SC JS runtime must to parse CSS again on any props changes.

Browser doesn't know how new styles will change different parts of the page. It needs to recalculate CSSOM again and reapply it to all competents.

BTW, I forgot forth problem. SC is not compatible with old CSS tools. You can use legacy Sass mixins in Astroturf. Or you can use Autoprefixer to polyfill Grid. SC doesn't support any PostCSS plugins.

@nhooyr
Copy link

nhooyr commented Jul 3, 2019

@nhooyr SC uses template string and put props inside this string. So SC JS runtime must to parse CSS again on any props changes.
Browser doesn't know how new styles will change different parts of the page. It needs to recalculate CSSOM again and reapply it to all competents.

According to https://medium.com/styled-components/announcing-styled-components-v4-better-faster-stronger-3fe1aba1a112

It's only milliseconds slower than css modules for a deep tree.

SC doesn't support any PostCSS plugins.

Thats because it handles prefixing itself and aside from that. The other plugins of postcss are cool but not critical.

I've opened callstack/linaria#445 to add a minimal runtime to Linaria for IE 11. Would be the perfect solution.

@satya164
Copy link
Contributor

satya164 commented Jul 3, 2019

You can check comparison of Linaria with SC here https://github.com/callstack/linaria/blob/master/docs/BENEFITS.md#advantages-over-other-css-in-js-solutions

Yea, the CSS parsing time by itself is fast, but there are several other considerations.

@giuseppeg
Copy link

giuseppeg commented Jul 3, 2019

One can already use styled-components with CRA right?

Linaria and Astroturf are the only more traditional libraries that can extract to static and do code splitting. They offer a styled-like API and such. IMHO dynamic styles based on props are a huge mistake that we all made (I am author of a couple of CSS in JS libs including ZEIT's styled-jsx). Toggling class names based on props or using inline styles is far better and more efficient.

Has anybody ever made a comparison table between Linaria and Astroturf? I'd go for one of the two.

@gaearon I feel like that this might be a never ending convo, I'd suggest that you either close the issue or come up with a resolution :)

@satya164
Copy link
Contributor

satya164 commented Jul 3, 2019

@giuseppeg

Toggling class names based on props or using inline styles is far better and more efficient.

Why is that more efficient than CSS variables?

Has anybody ever made a comparison table between Linaria and Astroturf

This only lists advantages of Linaria, but it's a start:

astroturfcss/astroturf#69 (comment)

@giuseppeg
Copy link

@satya164 yeah for theming custom properties are the best. Not sure how I feel about conditional styles and more complex things. Some use props to define sets of declarations (within a rule) conditionally or do crazy things with them.

@nhooyr
Copy link

nhooyr commented Jul 3, 2019

I've got a WIP for getting Linaria dynamic props working on IE.

See callstack/linaria#446

@giuseppeg
Copy link

@satya164 indeed I would lean towards Linaria (I guess mainly for the composition capabilities).

The only thing I dislike about it is the fact that it uses Stylis for preprocessing. I love Stylis but PostCSS is superior and less flacky. For build time solutions I'd pick PostCSS also because it supports plugins.

@giuseppeg
Copy link

FWIW if you only use props/custom properties for theming I wouldn't bother supporting IE11. I would just add a fallback and provide a single default theme (that's how I do at work).

@giuseppeg
Copy link

FWIW(2) https://github.com/threepointone/glam tried to implement a polyfill for IE11.

@nhooyr
Copy link

nhooyr commented Jul 3, 2019

FWIW if you only use props/custom properties for theming I wouldn't bother supporting IE11. I would just add a fallback and provide a single default theme (that's how I do at work).

How would you add a fallback cleanly?

@giuseppeg
Copy link

with this plugin https://github.com/postcss/postcss-custom-properties

@satya164
Copy link
Contributor

satya164 commented Jul 3, 2019

@giuseppeg

The only thing I dislike about it is the fact that it uses Stylis for preprocessing. I love Stylis but PostCSS is superior and less flacky.

It does use stylis by default. But the preprocessor is customizable: https://github.com/callstack/linaria/blob/master/docs/BUNDLERS_INTEGRATION.md#options

@jednano
Copy link

jednano commented Jul 3, 2019

Some use props to define sets of declarations (within a rule) conditionally.

@giuseppeg can you please explain why this is a bad thing or how it's less efficient? @satya164 what will Linaria do "under the hood" if one attempts to do something like this? Will a class be created for the condition?

const Foo = styled.div`
  color: black;
  ${props => props.error && {
    color: red;
    border: 1px solid red;
  }}
`

There's probably something wrong with my syntax.

It does use stylis by default. But the preprocessor is customizable: https://github.com/callstack/linaria/blob/master/docs/BUNDLERS_INTEGRATION.md#options

To be clear, @satya164, this means you can configure Linaria to use PostCSS as a preprocessor?

@giuseppeg
Copy link

giuseppeg commented Jul 3, 2019

@jedmao every time the prop evaluates to a different value the css in js library has to generate a new css rule and inject it in the stylesheet. So even though part of that rule is static (in your example color: black) you end up not reusing it.

And to clarify if you have a highly dynamic application (RTL, theming etc) probably doing these at runtime is way better, my point was that the more you can reuse (compose) the better but of course libraries cache aggressively and for many this is not a big deal!

@jednano
Copy link

jednano commented Jul 3, 2019

That color: black declaration, should it be duplicated, would not add any real footprint after gzip, FWIW. But I see your point, if that's indeed how it (Linaria) works under the hood.

I don't see the composition issue though. I've used BEM successfully before with ultimate composition, but I don't think I would lose composition with CSS in JS.

@satya164
Copy link
Contributor

satya164 commented Jul 4, 2019

@jedmao currently Linaria only supports interpolations for property values.

const Foo = styled.div`
  color: ${props => props.primary  ? 'blue' : 'black'}
`;

Internally it converts interpolations to CSS variables whose values are updated with the style property on the component.

this means you can configure Linaria to use PostCSS as a preprocessor

Yes

@jednano
Copy link

jednano commented Aug 27, 2019

I just published a craco plugin for Linaria, for those who don't want to eject from CRA, but still want CSS in JS.

@darthdeus
Copy link

darthdeus commented May 12, 2020

Is there any news on using astroturf with CRA without ejecting at this point? (other than the above mentioned craco plugin for Linaria)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.