CSS Hooks and the state of CSS-in-JS

leemeganj

Megan Lee

Posted on April 10, 2024

CSS Hooks and the state of CSS-in-JS

Written by Fimber Elemuwa✏️

As frontend applications and component-based architecture continue to grow in scope and complexity, one area that poses increasing challenges is writing modular, maintainable stylesheets. The cascading nature of traditional CSS makes it easy to end up with selector name collisions, dead code cruft, and difficulties encapsulating styles, and this has led more developers to turn to inline styles.

Inline styles allow you to bundle markup, CSS, and functionality of components in one place, avoiding the hassle of switching between your HTML and an external CSS file, and reducing the error-prone nature of indirect style references (via selector logic).

However, inline styles are generally unpopular due to their lack of separation issues, less reusability, and specificity issues. Added to the fact that media queries and pseudo-classes like :hover and :active cannot be used with inline CSS, the reservations developers have about inline styles are justified.

This led to the invention and rise of CSS-in-JS, which generates and injects style sheets into the DOM in real time. CSS-in-JS retains the encapsulation benefits of inline styles while regaining traditional CSS flexibility for dynamism, maintainability, and reusability. But like all things, it has its downsides, which brings us to CSS Hooks, a unique solution.

In this article, we’ll look in-depth at how CSS Hooks works and the advantages it delivers for component styling. We’ll explore where CSS Hooks fits in the broader CSS-in-JS ecosystem — which offers diverse techniques with their tradeoffs. We’ll also analyze the current state of CSS-in-JS regarding recent changes and the future direction across JavaScript frameworks.

What is CSS Hooks?

CSS Hooks is a React-based solution that allows you to declare stylistic rules using hooks. CSS Hooks brings the encapsulation and dynamism of CSS-in-JS to React apps while keeping the API familiar for React developers by using hooks. It makes features like scoped styles, media queries, etc., available via inline styles. This improves on many of the traditional pain points around writing CSS in JS.

This approach promotes cleaner and more maintainable code by keeping styles tied to their corresponding components, while under the hood, CSS Hooks handle dynamically injecting stylesheets and generating class names tied to components, providing per-component scoping without naming collisions.

How CSS Hooks works

CSS Hooks doesn’t try to reinvent the wheel by creating something new. It instead focuses on expanding what’s possible with inline styles, rather than fully replacing them with something different. You can now leverage a full range of dynamic CSS features with inline styles: pseudo-classes, complex selectors, media queries, and additional capabilities – all while keeping the simple, buildless approach inline styles are known for.

For example, CSS Hooks newly enables using the :hover or :active pseudo-classes and more – all without changes to existing syntax. It may seem too good to be true that inline styles can now support this level of dynamism and reuse. But CSS Hooks makes it a reality – no more runtime injection or build steps required. Here’s how it works.

CSSHooks works with React, Prereact, Solid.js, and Qwik, and we’re going to use Vite with the React configuration. First, let's create a project called css-hooks and install Vite:

$ npm create vite@latest css-hooks -- --template vanilla-ts
Enter fullscreen mode Exit fullscreen mode

Next, we’ll enter the folder we’ve created and install the CSS Hooks library:

cd css-hooks-playground
npm install && npm install @css-hooks/react
Enter fullscreen mode Exit fullscreen mode

So far, we’ll have a basic Vite template page that looks like this:

Vite Template Page

Next, we need to create our CSS Hooks system. To do this, we’ll create a css.ts file in the src folder and input the following code:

import { createHooks } from "@css-hooks/react";

export const { styleSheet, css } = createHooks({
  hooks: {
    "&:active": "&:active",
    "&:hover": "&:hover",

  },
  debug: import.meta.env.DEV,
});
Enter fullscreen mode Exit fullscreen mode

With this, we import the buildHooksSystem function from the @css-hooks/core library and then call the buildHooksSystem function to create the hook system. The resulting createHooks function will be used to define hooks and manage styles.

If you’re using CSS Hooks on a framework outside the ones mentioned above, you’d have to add a styleObjectToString function that takes a JavaScript object representing CSS styles, filters out invalid entries, formats the property names according to CSS conventions, and joins them into a single string, suitable for use as inline styles:

export function styleObjectToString(obj: Record<string, unknown>) {
  return Object.entries(obj)
    .filter(
      ([, value]) => typeof value === "string" || typeof value === "number",
    )
    .map(
      ([property, value]) =>
        `${/^--/.test(property) ? property : property.replace(/[A-Z]/g, x => `-${x.toLowerCase()}`)}: ${value}`,
    )
    .join("; ");
}
Enter fullscreen mode Exit fullscreen mode

This functionality is usually bundled within app frameworks, so if you’re using React, Prereact, Solidjs, or Qwik, you don’t have to add this to your css.ts file.

Next, we’ll go to the main.ts file and import the stylesheet into the app. We’ll go to the src/main.tsx file, import the stylesheet, and inject dynamically generated CSS styles using the styleSheet function:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { styleSheet } from './css.ts'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <style dangerouslySetInnerHTML={{ __html: styleSheet() }} />
    <App />
  </React.StrictMode>,
)
Enter fullscreen mode Exit fullscreen mode

With this, we can now use :hover and :active as inline styles within our code. To test this out, we will target the counter button, and update it so that it increases in size when clicked (active), and the color changes on hover, implementing all of these modifications using inline styles. To do that, we have to go to the app.tsx folder and add the following code to the button:

<button
          onClick={() => setCount((count) => count + 1)}
          style={css({
            transition: "transform 75ms",
            on: $ => [
              $("&:active", {
                transform: "scale(5)"
              }),
              $("&:hover", {
                background: "#1b659c",
              }),
              $("&:active", {
                background: "#9f3131",
              })
            ]
          })}
        >
          count is {count}
</button>
Enter fullscreen mode Exit fullscreen mode

Here’s the result:

Vite And React Counter App

With this, we’ve successfully installed and used CSS Hooks, an elegant solution to using pseudoclasses with inline styles.

The current state of CSS-in-JS

With component-based architecture becoming more prevalent in recent years, CSS-in-JS has become a mainstream approach for styling web applications, offering benefits like component encapsulation, dynamic styling, and improved developer experience. However, recent advances pose a significant problem for CSS-in-JS libraries.

The introduction of Server Components in React 18 and Next.js represents a shift towards optimizing the amount of JavaScript shipped to the browser. By having certain components rendered on the server, less JS needs to be hydrated on the client.

However, this poses a challenge for CSS-in-JS libraries, which depend heavily on client-side JavaScript and hydration. The dynamically generated styles from CSS-in-JS need to exist on page load to avoid a jarring style flash between server-rendered and hydrated output. Libraries like Styled Components handle this by injecting collected styles during the hydration process.

But with Server Components, there is now a split between the static server-rendered markup, and the remaining JS sent to hydrate the page. This fragmentation means most current CSS-in-JS solutions will face difficulties inferring which styles need to be injected during hydration, because both the styling code and components themselves may straddle server and client.

This could require refactoring on the part of CSS-in-JS libraries to cleanly separate server-rendered components which need pre-injected styles, versus client components that handle their hydration. Features like dynamic values also pose challenges for server rendering and hydration consistency, so new conventions around injecting styles for server components while avoiding duplication may be needed.

In summary, the shift towards server-side rendering encouraged by React and Next.js creates an urgency around refining CSS-in-JS hydration approaches. We’re now dealing with a split environment where both static and hydrated UI coexist, making it crucial to smooth out these integration processes.

Some libraries are already adapting to that, and CSS Hooks is one of those libraries well-positioned to address some of the challenges posed by server-side rendering and components. CSS Hooks’ architecture is built around component scoping, static extraction, deferred hydration modes, and universal rendering awareness to help smoothly adapt to the constraints posed by modern server-side rendering needs. CSS Hooks allows for building apps optimized for server rendering without giving up the advantages of CSS-in-JS techniques or requiring duplicate styling logic.

Conclusion

CSS-in-JS sits at a fascinating point in its evolution—with rapid innovation and a wide diversity of approaches, yet still in search of stability and standardization. New libraries continue emerging while existing ones iterate through various versions and syntax changes.

In many ways, CSS-in-JS represents the cutting edge of component styling. Yet with so many pieces still in motion, it may require another few years before clear winners emerge across the greater frontend ecosystem.

For now, CSS-in-JS operates more as a broad category of techniques than a unified approach, and for developers struggling with CSS shortcomings and scaling frontend code complexity, CSS Hooks provides a strong solution to level up component styling.


Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — start monitoring for free.

💖 💪 🙅 🚩
leemeganj
Megan Lee

Posted on April 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related