withastro/astro

Supporting CSS-in-JS

mayank99 opened this issue ยท 54 comments

It's not super clear which CSS-in-JS libraries work, so I'm creating this issue as sort of a place to start the conversation and document the current status.

Here's a few popular libraries that I know about (will keep this list updated):

Library Status Notes
styled-components ๐ŸŸก Partially works Prod build errors with: styled.div is not a function.
Can be worked around with client:only or by using buildSsrCjsExternalHeuristics and ssr.noExternal (will cause FOUC).
emotion ๐ŸŸก Partially works Prod build errors with: styled.div is not a function.
Can be worked around with client:only or by using conditional default import (will cause FOUC). Can also patch @astrojs/react.
linaria ๐ŸŸก Partially works Prod build errors with: Named export 'styled' not found.
Can be worked around using buildSsrCjsExternalHeuristics and ssr.noExternal or by downgrading to v3.
stitches ๐ŸŸก Partially works <style> tag for SSR needs to be in React component
typestyle โœ… Works -
vanilla-extract โœ… Works -
solid-styled ๐ŸŸก Partially works Causes FOUC
styled-jsx โŒ Doesn't work No vite or rollup plugin, requires babel
compiled โŒ Doesn't work No vite or rollup plugin, requires babel

From what I understand, if a library doesn't work in astro, it's because of one or more of these reasons*:

  • no support for vite/rollup
  • it requires babel
  • it uses context
  • it requires access to the renderer for SSR styles
  • it doesn't work with new react 18 apis

*I could be wrong so would be nice if someone can verify the above points.


Additionally, here's what @FredKSchott would like to see (quote message from discord):

  1. A table of which CSS-in-JS libraries are/aren't supported in Astro. If not supported, it would also list the reason. If supported, these could also have tests in the repo so that we don't break them in the future.
  2. Some time to go through the ones that aren't supported and try to fix any issues that are fixable by Astro.
  3. For any the css-in-JS libraries that still aren't supported after that, a clear error message inside of Astro core when it sees you try to import the package.

GitHub repo with examples:

https://github.com/mayank99/astro-css-in-js-tests

Participation

  • I am willing to submit a pull request for this issue.

This looks great! Is one of the goals of this to get documentation into the docs site?

This looks great! Is one of the goals of this to get documentation into the docs site?

Yup! After testing everything (including some more advanced cases) and fixing anything that's fixable by Astro, I believe we want to document what works, what doesn't work, and suggest alternatives.

I feel like it should be possible to fix styled-components and emotion. The big problem with both of them is that their SSR1 strategy requires altering the html produced by react's renderToString/renderToNodeStream methods.

The styled-components docs suggest wrapping the whole <App /> with a provider but only on the server. Theoretically, it should be possible to do this for every island in astro, but we don't have access to react's render methods in userland and we cannot do this with a wrapper component.2

As for emotion, the docs suggest two strategies. The default approach is supposed to work without any additional configuration, but I'm still seeing FOUC3 (this might be worth investigating). The advanced approach is similar to styled-components in that it needs to wrap the app in a provider and do a sort of "double render" to add the styles after the first render, and hydrate the styles on the client.

@matthewp Would it be possible for Astro to allow hooking into the rendering pipeline? That might solve both of these issues.

Footnotes

  1. SSR = server-side rendering but really I mean any form of prerendering, including static generation โ†ฉ

  2. From sc docs: "sheet.getStyleTags() and sheet.getStyleElement() can only be called after your element is rendered. As a result, components from sheet.getStyleElement() cannot be combined with into a larger component." โ†ฉ

  3. FOUC = flash of unstyled content โ†ฉ

@mayank99 Since these are for React apps I think it makes the most sense that this is part of some API for the React renderer. That might also require core Astro changes, I'm not sure.

There already is a hook into the rendering pipeline via the renderer API, so it might be possible to prototype this idea as a separate renderer before plugging it into the React render (although either approach is reasonable).

You'd want it to work with streaming, it looks like styled-components has as solution for that: https://styled-components.com/docs/advanced#streaming-rendering

I was starting to look into customizing the renderer when I decided to try the prod build (which I had not tried before), and unfortunately the situation is even worse. See updated chart in issue description.

styled-components and emotion both fail with: styled.div is not a function. Adding them to vite.ssr.noExternal does not seem to help. @linaria/css works fine but @linaria/react fails with: Cannot use import statement outside a module.

There are existing issues in all three repos (styled-components/styled-components#3601, emotion-js/emotion#2730, callstack/linaria#1054) but I couldn't get the suggested workarounds to work. ๐Ÿ™

Edit: these can be worked around using buildSsrCjsExternalHeuristics.

Another lib worth trying is astroturf, which would be good fit for this project just for the name alone ๐Ÿ™ƒ
https://github.com/4Catalyzer/astroturf
https://4catalyzer.github.io/astroturf/introduction/

@nstepien I did look into astroturf but excluded it from the list because there is no vite/rollup plugin and the library does not look like it's maintained anymore.

I've not tried it myself, but the docs do mention a rollup plugin: https://4catalyzer.github.io/astroturf/setup

Also btw https://github.com/typestyle/typestyle worked fine when i tried it.

@osdiab I've added an example for typestyle and updated the table. Thanks for the recommendation.

@mayank99 if you have failing Stackblitz examples for the ones that are possibly bugs that would be helpful for debugging.

@matthewp You should be able to open the examples repo in stackblitz.

https://stackblitz.com/github/mayank99/astro-css-in-js-tests/tree/main/styled-components
https://stackblitz.com/github/mayank99/astro-css-in-js-tests/tree/main/vanilla-extract

Swap out the last part of the url with the name of the library you want to debug.

https://stackblitz.com/github/mayank99/astro-css-in-js-tests/tree/main/styled-components
                                                                       ^^^^^^^^^^^^^^^^^

In solidjs/solid-styled-components#27, I saw mentioning of running extractCSS or similar methods for css-in-js libs. Did anyone take a look in something similar?
This is my failed attempt at it trying to get goober working. As is, it will work only when client:load is enabled for a component: https://stackblitz.com/edit/github-rqtizn-easxa3?file=astro.config.mjs

Basically, we need to put the css from extractCss called to a style in head with id _goober but now sure how to do that with tr

We are also evaluating Astro now for SSR at @RapidAPI and this seems a huge blocker, since we rely on emotion in our ui lib

If you guys can get styled components to work with Astro I am totally going to make the switch. I love styled components, works perfectly with component based styling and makes your code so easy to read.

vanilla-extract now supports Astro! vanilla-extract-css/vanilla-extract#796

I've updated the table with the most recent status. A lot more green now ๐Ÿ’š

In addition to official vanilla-extract support, I was able to work around the build errors in styled-components/emotion/linaria by using a combination of vite.legacy.buildSsrCjsExternalHeuristics and vite.ssr.noExternal (see commit).

styled-components/emotion will still cause FOUC so the next step would be to look into injecting styles into the SSR output (detailed in my earlier comment).

Hey @mayank99
I was just giving your Stitches example a try. Am I correct in assuming that you can not abstract the head component into a layout and add the Styles component in there? When trying this I see styles from page 1 leaking into page 2, causing unnecessary styles to be in that page. So it seems that during build Astro is keeping the layout (and thus the head), and the styles will get added on top of the existing ones. Especially in bigger apps I'd rather not repeat all of the head content.

Thanks for this great overview ๐Ÿ™

Edit:
Oh actually, when removing the layout and creating 2 pages with each their own head with the styles included as in the example, it still doesn't work. Somehow it now even flips the styles (eg I have a colored button on page 1, but those styles are now on page 2 and now on page 1 in the build) ๐Ÿค”

I'm not sure if it's normal but there is some FOUC (in dev mode only) with Astro + vanilla-extract
Taking the example from @mayank99 :

CleanShot.2022-09-15.at.08.42.56.mp4

No FOUC when building the app in production mode.

@jon301 This is related to the Vanilla Vite plugin itself, same behaviour can be observed in SvelteKit.

@mayank99 , thank you for great overview and for the Stackblitz examples!

I needed to make Astro work with Emotion, because I use Material UI in my website. Even if I've could use also styled-components, the default approach of Emotion seemed interesting to me, pretty simple and no big changes needed.

I tried your repo with examples and somehow the workaround for styled.div is not a function that you found (buildSsrCjsExternalHeuristics one) didn't work. I found a shameful workaround of myself just to not get stuck:

import styledImport, { CreateStyled } from '@emotion/styled';
let styled = typeof styledImport.default !== 'undefined' ? styledImport.default as any as CreateStyled : styledImport;

Then I've got it working but with FOUC. Emotion didn't render <style> tags alongside the components on the server. It was happening because Astro React integration uses renderToPipeableStreamAsync, but Emotion doesn't support it yet - it's still an open feature request. But it supports (the now deprecated) renderToNodeStream that actually does the job - full Suspense support, but without streaming (according to this ).

So my second workaround was to patch @astrojs/react server.js and replace renderToPipeableStreamAsync with renderToNodeStreamAsync (a new function with a similar implementation to renderToStaticNodeStreamAsync). Then FOUC was gone.

There are 3 ways forward for Emotion that I see:

  1. Try to hook into Astro rendering to see if advanced approach works
  2. Wait when emotion will support renderToPipeableStream. But it seems not so easy, and most probably it will not be available with the default approach, but only with advanced approach. So we would probably need to hook into Astro rendering anyway.
  3. Somehow be able to configure Astro React integration to use renderToNodeStream. That would not do any damage, IMHO. Even if renderToNodeStream supports Suspense without streaming... astro react integration waits anyway for the whole stream to end before going forward. Of course using deprecated stuff is not OK, but making it user opt-in should be fine.

What do you think we should do?

@igorbt Thanks for looking into this, great work!

the workaround for styled.div is not a function that you found (buildSsrCjsExternalHeuristics one) didn't work.

So in my repo I'm using a conditional vite config, and it doesn't seem to get the correct values. It works in prod (but not in dev) if I just do this:

vite: {
	legacy: { buildSsrCjsExternalHeuristics: true },
	ssr: { noExternal: ['@emotion/*'] },
}

I found a shameful workaround of myself just to not get stuck:

import styledImport, { CreateStyled } from '@emotion/styled';
let styled = typeof styledImport.default !== 'undefined' ? styledImport.default as any as CreateStyled : styledImport;

This is actually great! I think I tried a variation of this and couldn't get it to work, so I'm happy you were able to. This is better than buildSsrCjsExternalHeuristics imo, so I will update the table.

  1. Try to hook into Astro rendering to see if advanced approach works

I think this would help with styled-components too (as I mentioned earlier). I'm not sure what would be the best way to expose this to users, outside of asking them to make a customized version of the react integration.

  1. Somehow be able to configure Astro React integration to use renderToNodeStream. That would not do any damage, IMHO. Even if renderToNodeStream supports Suspense without streaming... astro react integration waits anyway for the whole stream to end before going forward. Of course using deprecated stuff is not OK, but making it user opt-in should be fine.

I agree, it should be opt-in if astro decides to use renderToNodeStream. But I also feel like patch-package might be enough since this is a small change.

@matthewp What do you think?

It seems advanced approach for Emotion SSR only works with ReactDOM. renderToString, so I guess it will not work with current Astro React integration.

For styled-componets situation is almost the same as with emotion - there is no support for new React SSR API renderToPipeableStream (here is the open issue for this). There are some signs that authors are working towards it. And there is a workaround, that basically buffers the stream, so no real support for streaming. Probably we could use that workaround, but hooking into Astro rendering.

Actually React team stated pretty clear that the architectural changes for v18 will make it pretty hard for CSS-in-JS libs to get along: reactwg/react-18#110 and they even advice for "You could however build a CSS-in-JS library that extracts static rules into external files using a compiler".

On a more pragmatic note, because Astro mainly uses SSG, the streaming support is not important, and using (deprecated) renderToNodeStream might be a temporary solution. But for Astro SSR use-case I'm not sure how important the streaming support is.

I can confirm that the astro + solid-styled-components (goober solid wrapper) + twin.macro works nicely in both SSR and CSR mode, and also in both dev and prod settings

@mayank99 if this only requires some config changes I think having this added to the React integration makes sense.

I can confirm that the astro + solid-styled-components (goober solid wrapper) + twin.macro works nicely in both SSR and CSR mode, and also in both dev and prod settings

@guiguan

Well, don't leave us in the dark! How did you set it up?

I can confirm that the astro + solid-styled-components (goober solid wrapper) + twin.macro works nicely in both SSR and CSR mode, and also in both dev and prod settings

@guiguan

Well, don't leave us in the dark! How did you set it up?

he's just kidding LOL

@igorbt

Hi! I tried your solution with the patch, and it worked for me too, with the styled API. Sadly we don't use that approach and I'd like to get this working with the css prop. Do you have an idea how to get that to work?

@Pety99, to make it work with css prop the key is to configure jsxImportSource to @emotion/react.

In theory there should be multiple ways to do this properly: setting this in tsconfig.json, using a pragma /** @jsxImportSource @emotion/react */, overriding vite.esbuild config in the astro.config.mjs, but none of them worked for me. It seems that @astrojs/react overrides everything that comes from userland settings, so the only way I could make it work is to, again, patch the library :(.

So, go to @astrojs/react/dist/index.js and replace react with @emotion/react for jsxImportSource and importSource configurations.

@igorbt Thank you so much!

Unfortunately Linaria seems broken again with latest Astro. The example setup results in the component missing:

image

Did you manage to have css prop working in .astro files with Stitches? It doesn't seem to work.
I have tried to replicate solution mentioned here but without any success. Stitches might do things the other way under the hood.
Also I'm struggling with the issue of styles live reload so styles placed in head tag (as React component) are not always up to date - sometimes I have to reload page manually. On the other hand it works flawlessly with pure Vite so it might be astro-ish issue I guess.

y-nk commented

could we add styled-jsx to this?

could we add styled-jsx to this?

@y-nk i was looking into it and could not figure out how i would use styled-jsx here. looks like it only works with babel: https://github.com/vercel/styled-jsx#getting-started

updated table to mention it though.

Unfortunately Linaria seems broken again with latest Astro. The example setup results in the component missing

@soluml Looks like linaria now has a vite plugin: @linaria/vite. They have an example of astro+solid in their repo. I could not get it to work with react, I was getting all sorts of errors when I tried it. Maybe someone else might be able to figure it out.

I'm probably late to the party but solid-styled now supports Astro since 0.7.0 via Vite plugin.

Context usage is now optional, the only hurdle now is pre-rendering the styles, which I have no idea how to achieve in Astro.

hey @lxsmnsyc, you're not late at all. we're all still trying to find ways to draw rectangles even in the year of our lord 2023 ๐Ÿ˜„ i've updated the table in the PR description as well as the solid-styled example in my repo. thanks

+1. client:only is slow, and client:load shows unstyled first

For runtime CSS-in-JS like styled-components and Emotion, does client:load work?

If I'm not mistaken, client:only is basically CSR and obviously it will work no matter what. And for client:load, it sounds like it's SSR with hydration. In theory, it should work as well. And it's a (much) better option than client:only if it does.

igl commented

Not having great styled-components support is such a deal breaker for astro :(

90% of my projects use MUI because it's just the best & most complete library out there.
This is literary the only thing that prevents me from switching to Astro.

Would love to see support for MUI as well

@Pety99, to make it work with css prop the key is to configure jsxImportSource to @emotion/react.

In theory there should be multiple ways to do this properly: setting this in tsconfig.json, using a pragma /** @jsxImportSource @emotion/react */, overriding vite.esbuild config in the astro.config.mjs, but none of them worked for me. It seems that @astrojs/react overrides everything that comes from userland settings, so the only way I could make it work is to, again, patch the library :(.

So, go to @astrojs/react/dist/index.js and replace react with @emotion/react for jsxImportSource and importSource configurations.

Works like a charm 2023-03-29

Here is some docker help for anyone that needs

# Fix Astro Vite issue compiling JSX
# https://github.com/withastro/astro/issues/4432#issuecomment-1309770060
RUN sed -i 's/jsxImportSource: "react"/jsxImportSource: "@emotion\/react"/' ./node_modules/@astrojs/react/dist/index.js
RUN sed -i 's/importSource: ReactVersion.startsWith("18.") ? "react" : "@astrojs\/react"/importSource: ReactVersion.startsWith("18.") ? "@emotion\/react" : "@astrojs\/react"/' node_modules/@astrojs/react/dist/index.js

When should we expect emotion to be fully operational? This is the only problem that stops me from using astro atm

just a reminder that any "+1"/"ETA?" or similar comments are unhelpful and spammy.

if you would like this issue resolved for your use case, the best way to do that is to work with the maintainers of both tools and try to contribute.

beyond that, i want to share two things:

  • you might be able to patch the astro react integration to use the old renderToNodeStream api. see comment above from igorbt: #4432 (comment)
  • there is an ongoing effort to support new react apis in SC/emotion, but it might take some time as they are waiting for new apis from the react team. see comment from Andarist (maintainer of emotion): styled-components/styled-components#3658 (comment)

Emotion works for static site generation (on the server side (and of course on the server side in general as well)). Here is an example:

---
import {css} from '@emotion/css'

const tag = css`
	border: 1px solid;
`

import {extractCritical} from '@emotion/server'

const mainKey = 'c', key = mainKey + 'ss-'

, className = name => ({
	class: name.replace(key, mainKey)
})
---

<style set:html={
	extractCritical(
		[
			tag
		]
			.join(' ')
	)
		.css
			.replaceAll(
				...[
					key, mainKey
				].map(
					value => '.' + value
				)
			)
}/>
<div {...className(tag)}/>

 
 
related documentation:

Anyone could figure out how to avoid FOUC on Next.js 13, App router, typescript, Twin macros?

Anyone could figure out how to avoid FOUC on Next.js 13, App router, typescript, Twin macros?

Bro's asking a nextjs question in an astro issue ๐Ÿ’€

So Emotion definitely works for static site generation (on the server side (and of course on the server side in general as well)): Here is a full example:

 
example:

index/

gsipos commented

Hello!
I think i found an another workaround for mui and emotion, but it is probably not very elegant.
So I basically render the React component with renderToString and extract the emotion css stuff and concatenate them together.

import createCache from '@emotion/cache'
import createEmotionServer from '@emotion/server/create-instance'
import * as ReactDOMServer from 'react-dom/server'
import { CacheProvider } from '@emotion/react'

export const extractEmotionCss = (component: () => React.ReactNode) => {
  const cache = createCache({ key: 'mui-emotion-cache' })
  const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache)

  const html = ReactDOMServer.renderToString(
    <CacheProvider value={cache}>
      {component()}
    </CacheProvider>,
  )

  const emotionChunks = extractCriticalToChunks(html)
  const emotionCss = constructStyleTagsFromChunks(emotionChunks)
  return `${emotionCss} ${html}`
}

after this, I have an ExtractEmotionStyles.astro file:

---
import { extractEmotionCss } from '../functions/extract-emotion-css';

export interface Props {
  component: () => React.ReactNode;
}

const component = Astro.props.component;
const content = extractEmotionCss(component);

---

<Fragment set:html={content} ></Fragment>

and with this I can just use it in other astro files, like this:

<ExtractEmotionStyles component={MyMuiReactComponent} />

I'm fairly new to Astro and the SSG/SSR stuff, so please tell me if this solution is problematic.

Mantine has FOUC, have to refresh the page after hyperlink transition for CSS to render.

@grctest the next Mantine version, v7 currently in alpha, does away with CSS-in-JS altogether and works with Next.js app directory SSR. Upgrading to Mantine v7 when it's out will likely be the best solution going forward.

Moving this issue to a discussion on our roadmaps repo.