• Stars
    star
    119
  • Rank 297,930 (Top 6 %)
  • Language
    JavaScript
  • Created 10 months ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

My Dream CSS-in-JS Tool

The short version: styled-components, but without a client runtime. Built for Next.js.

The goal is to build something that is fully compatible with React Server Components. We shouldn't need to add the "use client" directive in order to add styled components.

This does require making some sacrifices. For example, it probably couldn't support dynamic prop interpolations, and it definitely couldn't support ThemeProvider. Instead, we'll use CSS variables for dynamic content and theming.

Here's a quick example:

// app/components/Demo.js
import styled from 'dream-tool';

function Demo({ width }) {
  return (
    <Wrapper style={{ '--color': width > 500 ? 'red' : undefined }}>
      Hello world
    </Wrapper>
  );
}

const Wrapper = styled.section`
  display: flex;
  justify-content: center;
  color: var(--color);
`;

Presuming that this is the only component in the project, here's what the resulting server-rendered HTML should be:

<html>
  <head>
    <style>
      .app_components_Demo_Wrapper {
        display: flex;
        justify-content: center;
        color: var(--color_);
      }
    </style>
  </head>
  <body>
    <section class="app_components_Demo_Wrapper">Hello world</section>
  </body>
</html>

The classes are derived from the filename. This ensures global uniqueness, and should allow us to reference these components like this:

const Quote = styled.blockquote`
  font-style: italic;
`;

const Link = styled.a`
  /* Default styles */
  color: var(--color-primary);

  /* Styles when Link is inside Quote: */
  ${Quote} & {
    color: inherit;
  }
`;

The resulting CSS should be:

.app_components_Demo_Quote {
  font-style: italic;
}

.app_components_Demo_Link {
  color: var(--color-primary);
}
.app_components_Demo_Quote .app_components_Demo_Link {
  color: inherit;
}

This, in a nutshell, is what I wish existed. A fully static CSS-in-JS approach that allows us to reference one component within another.

(I realize that in a larger application, the file paths would be quite long. Maybe gzip will help us out here. If not, we can always hash the paths in production.)

Generating the class names

I believe we'll need to have a compile step that will generate these class names. I'm imagining a Babel (or SWC?) plugin that does the following transformation:

// Before
const Wrapper = styled.div`
  color: red;
`;

// After
const Wrapper = styled('div', 'app_components_Demo_Wrapper')`
  color: red;
`;

This way, the styled component receives this class name, and can do the work of preparing the CSS during the server-side render.

Applying the CSS

This is where things get tricky. πŸ˜…

The very first thing I tried looked like this:

'use client';
import React from 'react';
import { useServerInsertedHTML } from 'next/navigation';

export default function styled(Tag, css, compiledClassName) {
  return function StyledComponent(props) {
    useServerInsertedHTML(() => {
      return (
        <style>{`
        .${compiledClassName} {
          ${css}
        }
      `}</style>
      );
    });

    return <Tag className={compiledClassName} {...props} />;
  };
}

The useServerInsertedHTML hook is provided by Next as a way to inject some stuff into the <head> of the generated HTML file, during Server Side Rendering.

There's two problems with this approach:

  1. useServerInsertedHTML requires the "use client" directive, which means that any file that imports it would also need to be a Client Component. This means we can't use this approach in a Server Component.
  2. By doing it right in the styled-component, it means that every single style would generate its own <style> tag, which would bloat the HTML file. Ideally, they should all be collected within a single <style> tag.

To solve the 2nd problem, CSS-in-JS libraries in Next.js use a "registry" approach. A registry is a component that wraps around the entire application, and is designed to collect and apply the styles created by descendants.

It struck me that it should be possible for this "registry" component to be the only Client Component we need for this stuff, and it would create the only <style> tag, collecting all of the styles emitted during that first server-side render.

The tricky thing is, how do we actually collect those styles in a Server Component?

My first idea was to use React Context. Maybe we could do something like this:

export const StyleContext = React.createContext();

function StyleRegistry({ children }) {
  const collectedRules = React.useRef([]);

  useServerInsertedHTML(() => {
    const styles = collectedRules.current.join('\n');
    return <style>{styles}</style>;
  });

  return (
    <StyleContext.Provider value={collectedRules}>
      {children}
    </StyleContext.Provider>
  );
}

In this made-up example, we collect all of the CSS rules in an array, stored in a ref. That ref is made available via React context.

Each styled-component would then push their rules into this array:

'use client';
import React from 'react';

import { StyleContext } from './Registry';

export default function styled(Tag, css, compiledClassName) {
  return function StyledComponent(props) {
    const collectedRules = React.useContext(StyleContext);

    collectedRules.current.push(`
      .${compiledClassName} {
        ${css}
      }
    `);

    return <Tag className={compiledClassName} {...props} />;
  };
}

As I understand it, useServerInsertedHTML runs after all of the descendants have been rendered, but it still runs on the server (unlike useEffect). And so, when that code runs, collectedRules.current will be an array of strings, each one for a different CSS rule. They'd be concatenated into a string, and applied in a <style> tag.

Unfortunately, React Context doesn't work in Server Components. In fact, React Server Components doesn't seem to have any mechanism that allows descendants to pass data up to the "registry" ancestor.

This appears to be a known issue with React Server Components. The RFC mentions that this is an active area of research.

I learned about this in an issue in the styled-components repo. This appears to be the main blocker.

A thread from Sebastian MarkbΓ₯ge shares another interesting possible solution: the React.cache() API. React.cache essentially provides a per-request cache, available exclusively in Server Components.

I did a quick and dirty prototype with this, and it actually works perfectly, but only in Server Components. I got all excited, only to realize that it breaks when you try and use it in Client Components:

'use client';

function App() {
  return <Button>Hello World</Button>;
}

// 🚫 Error: Not Implemented
const Button = styled.button`
  color: red;
`;

This repository includes my dirty proof-of-concept. I'm hoping that someone who understands all of this stuff a lot better than me can help figure out a workable solution, if such a thing exists!

Here are the relevant files:

  • styled.js β€” equivalent to styled from styled-components.
  • StyleRegistry.js β€” The registry that manages the cache. Server Component.
  • StyleInserter.js β€” The component that injects the styles into the page, using useServerInsertedHTML. Client Component.

Comparison with existing tools

There are several CSS-in-JS tools which don't require a runtime, and are (or could be) compatible with RSC.

For example, Panda CSS works by statically analyzing your code and extracting a set of atomic utility classes into a CSS file. This is really cool, but it doesn't quite work for me.

The biggest issue is that it doesn't support component referencing:

const Quote = styled.blockquote`
  font-style: italic;
`;

const Link = styled.a`
  /* Default styles */
  color: var(--color-primary);

  /* 🚫 Doesn't work in Panda CSS */
  ${Quote} & {
    color: inherit;
  }
`;

That's the biggest blocker for me, but there are a couple of other things I've noticed:

  • As far as I can tell, Panda CSS doesn't support route-specific CSS. Every style in every component across the entire application is compiled into a single CSS file. I could be wrong about this though, I only did a quick test.
  • Panda CSS compiles to Tailwind-style utility classes. This certainly helps to reduce the filesize of that CSS file, since there are no duplicate CSS declarations, but I'm not a fan of the in-browser debugging experience, where each CSS rule consists of a single declaration.

There's also Linaria, which has been around for quite a while, and provides a styled-component-like API that compiles to CSS files. The next-with-linaria package adds support for the Next.js App Router, by cleverly compiling the CSS into CSS Modules, which already have first-class support in Next.js.

Linaria + next-with-linaria is surprisingly great. It supports component referencing, and the output is identical to that of CSS Modules. It's honestly pretty close to exactly what I want. The only little nitpick I could find is that it isn't optimized for Suspense; all of the CSS for a given route is compiled into 1 CSS file, rather than streaming in additional CSS along with the extra HTML/JS.

The trouble is that next-with-linaria is really more of a prototype than a production-ready library. There's a big warning in the README that warns not to use it in production.

I also worry about its longevity; Linaria uses Webpack 5, along with Webpack-specific features like Virtual Modules. Next is in the process of migrating to Turbopack, and so if/when Next drops support for Webpack, it would break this library. It also means that bundling is presumably slower, since it has to use Webpack instead of Turbopack (which is written in Rust and designed to be fast).

I think my ideal CSS tool would not require any sort of bundler integration: I believe it should be sufficient to have a Babel/SWC transform, to generate the class names. The tool I'm imagining would run during the server-side render rather than at compile-time, producing a <style> tag rather than a linked CSS file, containing only the styles necessary for the current UI.

Other things to consider

  • This tool should also work with Suspense and Streaming SSR; the <style> tag should be updated when different parts of the page are streamed in around Suspense boundaries. Fortunately, it seems that useServerInsertedHTML already tackles this.
  • In terms of CSS preprocessing, I think it makes sense to use Lightning CSS. It offers several improvements over Stylis, the preprocessor used by styled-components:
    • It's faster (written in Rust).
    • Vendor prefixing isn't an "all or nothing" equation, we can target specific browsers using browserslist.
    • It does babel-style transpiling for several modern CSS features.

More Repositories

1

react-flip-move

Effortless animation between DOM changes (eg. list reordering) using the FLIP technique.
JavaScript
4,080
star
2

guppy

🐠A friendly application manager and task runner for React.js
JavaScript
3,268
star
3

use-sound

A React Hook for playing sound effects
JavaScript
2,614
star
4

waveforms

An interactive, explorable explanation about the peculiar magic of sound waves.
JavaScript
1,430
star
5

panther

Discover artists through an infinite node graph
JavaScript
919
star
6

new-component

βš› ⚑ CLI utility for quickly creating new React components. ⚑ βš›
JavaScript
699
star
7

redux-vcr

πŸ“Ό Record and replay user sessions
JavaScript
585
star
8

key-and-pad

🎹 Fun experiment with the Web Audio API 🎢
JavaScript
361
star
9

Tello

🐣 A simple and delightful way to track and manage TV shows.
JavaScript
329
star
10

tinkersynth

An experimental art project. Create unique art through serendipitous discovery.
JavaScript
282
star
11

beatmapper

A 3D editor for creating Beat Saber maps
JavaScript
271
star
12

blog

OLD VERSION of the joshwcomeau.com blog. Kept for historical purposes.
JavaScript
236
star
13

dark-mode-minimal

JavaScript
170
star
14

react-retro-hit-counter

πŸ†• Go back in time with this 90s-style hit counter.
JavaScript
162
star
15

redux-sounds

Middleware for playing audio / sound effects using Howler.js
JavaScript
130
star
16

react-collection-helpers

A suite of composable utility components to manipulate collections.
JavaScript
106
star
17

redux-favicon

Redux middleware that displays colourful notification badges in the favicon area.
JavaScript
105
star
18

nice-index

Atom package to rename `index.js` files to their parent directory names
CoffeeScript
82
star
19

react-europe-talk-2018

JavaScript
64
star
20

fakebook

A front-end Facebook clone, built with React and Redux
JavaScript
52
star
21

talk-2019

Slides for my 2019 talk, "Saving the Web 16ms at a Time"
JavaScript
52
star
22

understanding-react

Daily exploration of the React source code
42
star
23

talon-commands

Python
38
star
24

return-null

My React Europe 2017 lightning talk
JavaScript
35
star
25

explorable-explanations-with-react

JavaScript
35
star
26

word_dojo

JavaScript
18
star
27

react-boston-2018

My ReactBoston 2018 talk, The Case for Whimsy (Extended mix)
JavaScript
16
star
28

netlify-serverless-demo

JavaScript
16
star
29

css-for-js-flow-layout

HTML
14
star
30

whimsical-mail-client

JavaScript
14
star
31

ColourMatch

Search by Colour. Find photos with matching palettes.
CSS
12
star
32

talk-2020-react-europe

JavaScript
10
star
33

react-europe-workshop-confetti

JavaScript
10
star
34

plot

Experiments in pen plotting and generative art
JavaScript
9
star
35

sandpack-bundler-beta

JavaScript
8
star
36

react-play-button

JavaScript
7
star
37

react-europe-workshop-travel-site

JavaScript
7
star
38

deployed-screensaver

JavaScript
7
star
39

Uncover

πŸ“š Aggregate new releases from your favourite authors. Built with Vuejs and Node
Vue
6
star
40

redux-vcr-todomvc

ReduxVCR integrated into TodoMVC.
JavaScript
5
star
41

react-letter-animation

A take on Mike Bostock's General Update Pattern, using React Flip Move.
JavaScript
5
star
42

Perseus

Gather info about your stargazers. Uses the GitHub GraphQL API
JavaScript
5
star
43

gatsby-preview-demo

Gatsby starter for a Contentful project.
JavaScript
5
star
44

words-with-strangers-redux

A universal redux version of my Meteor attempt at Words with Friends (online scrabble).
JavaScript
4
star
45

leitner

Keep track of your position in the 64-day Leitner calendar
JavaScript
4
star
46

empowered-development-with-gatsby

My Gatsby Days LA 2020 talk!
HTML
4
star
47

react-floaters

Spring-based scroll animation experiment with React.js
JavaScript
4
star
48

datocms-Gatsby-Portfolio-Website-demo

CSS
4
star
49

tetris

A simple tetris clone, in React and Redux, using Redux Saga
JavaScript
4
star
50

katas

A bunch of CodeWars challenge solutions. Part of an ongoing blogging effort at https://medium.com/@joshuawcomeau
JavaScript
4
star
51

react-europe-workshop-twitter-like

JavaScript
4
star
52

joshbot

The Discord bot for my Course Platform's community.
JavaScript
3
star
53

unlikely-friends

Don't mind me. Experiments with Gatsby themes
JavaScript
3
star
54

dont_eat_here_toronto

A Chrome extension that displays Toronto DineSafe restaurant inspection stuff on Yelp restaurant pages.
JavaScript
3
star
55

script-search

Find code used on the world's top sites
Python
3
star
56

basilica

JavaScript
3
star
57

yger

πŸš€βš‘οΈ Blazing fast blog built with Gatsby and Cosmic JS πŸ”₯
JavaScript
3
star
58

gatsby-dark-mode

CSS
2
star
59

Mars-Rover-HTML

An HTML/CSS Mars Rover simulation
CSS
2
star
60

generic-portfolio

An example of a generic portfolio (what NOT to do)
HTML
2
star
61

ember-todo

Don't mind me! Just a toy app to familiarize myself with Ember
JavaScript
2
star
62

mono-gatsby-apps

CSS
2
star
63

Aracari

A simple-as-possible budgeting web app. Because I suck at budgeting.
JavaScript
2
star
64

temp-project-wordle

JavaScript
2
star
65

AngelHack_rando

1st Place @ AngelHack TO. Built in 24h.
Ruby
2
star
66

tree-shake-test

JavaScript
2
star
67

gatsby-personalization

CSS
2
star
68

ssr-repro

CSS
2
star
69

react-fluid-window-events

React component for smooth, efficient resize/scroll handling.
JavaScript
2
star
70

Percentext

a jQuery plugin that lets you style text elements by width.
JavaScript
2
star
71

RequestKittens

The only API ridiculous enough to let you find cats by emotion.
JavaScript
2
star
72

book-demo

Demo of Git fundamentals
2
star
73

HungryBelly

An extension of the winning 24-hour project created for AngelHackTO
Ruby
1
star
74

art

Generative art experiments
JavaScript
1
star
75

elevator-simulator

WIP
JavaScript
1
star
76

RAFT

Utility for efficient, organized window-level event handlers
JavaScript
1
star
77

CLYWmparison_blogembed

A Yoyo Comparison tool, used by Caribou Lodge Yoyo Works
JavaScript
1
star
78

TicTacToe

JavaScript
1
star
79

TeeVee

A simple Meteor app to help me keep track of which episodes of TV shows I've seen.
JavaScript
1
star
80

RequestKittensDocs

The documentation / sales site for the RequestKittens API
JavaScript
1
star
81

foodshow

A silly weekend project, using the Unsplash API to display a food slideshow.
JavaScript
1
star
82

egghead-optimized-images-1

HTML
1
star
83

react-simple-canvas

React components that replicate the SVG interface, but renders to an HTML5 Canvas
JavaScript
1
star
84

munsell-colors

JavaScript
1
star
85

joshwcc

My portfolio/blog. Nowhere close to done yet.
Ruby
1
star
86

egghead-optimized-images-2

HTML
1
star
87

learn-webgl

Experiments for education with WebGL. Don't mind me.
JavaScript
1
star
88

Crowdfunder

A Kickstarter clone. Bitmaker Labs final assignment.
Ruby
1
star
89

MEAN_stack_starter

A ready-to-go initialized MEAN stack with tons of customizations.
CSS
1
star
90

egghead-videos

JavaScript
1
star
91

pixelminer

An idle game (Γ  la cookie clicker), built to help me experiment with flowtype.
JavaScript
1
star
92

huddle

A Meteor app that aims to help patients have better access to their medical files, and get second opinions from physicians on the platform.
CSS
1
star
93

joshwcc_ver2

Attempt #2 at the joshw.cc portfolio site.
Ruby
1
star
94

Some-new-project

1
star
95

Advent-of-Code-2016

JavaScript
1
star
96

confetti-temp

JavaScript
1
star
97

redux-server-persist

JavaScript
1
star
98

Tori

Twitter, but for haikus.
JavaScript
1
star
99

classroom-q

Gatsby experimentation
CSS
1
star
100

fntest

CSS
1
star