Scoped CSS in Remix.run apps

Scenario

While rewriting this website with remix.run and coming from the Angular universe with some React experience, one of the major things I was missing were scoped component styles. The /styles folder got cluttered quickly as I added more and more global styles for the component with class prefixes and so did the root.tsx file. So I decided that it's necessary to do some more research and refactor what I have got so far.

And the solution is actually a lot easier than I expected. Imagine you have got the default folder structure for remix projects in your workspace:

/app
  /components
  /routes
  /styles
  /utils
  entry.client.tsx
  entry.server.tsx
  root.tsx

The goal is to be able to create reusable components in the /components folder with their styles right next to them.

How remix styling works

The goal of styling in remix is, to have only the styles defined that are necessary for the page. This is a optimisation for server-side rendering to deliver as few kB as possible. In remix you define a mix of custom components that live inside the /components folder and routing components that live inside the /routes folder. The routing components automatically set up the router tree for the whole application based on the file system structure and the naming of the files.

Based on the remix docs it is recommended to place the stylesheets for these routing components in the /styles folder. So, how do the styles get recognized by remix? There is a special component called Links. It's imported in the root.tsx file by default

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta />
    <Links />
    <!-- This is the one! -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

This Links component get's replaced by all links functions used in routing components. Let's have a look at these functions. Right before the component definition you can create a new function called links. This function returns an array of objects. Each of these objects must contain the properties { rel: 'stylesheet', href: CSS_IMPORT }, where CSS_IMPORT must be replaced with some css import, like in the following example.

import { LinksFunction } from 'remix';
import globalStyles from '~/styles/global.css';

export const links: LinksFunction = () => [
  { rel: 'stylesheet', href: globalStyles },
];

Example: An alert component

Creating the component

Imagine we write a custom alert component. Then place two files in the /components folder:

/app
  /components
    /alert
      alert.tsx
      alert.css

Let's jump in to the tsx file and create the most simple component. It has one property called message where we can tell the component which text to display.

interface AlertProps {
  message: string;
}

export function Alert(props: AlertProps) {
  return <div class="alert">{props.message}</div>;
}

In the next step we want to create really nice looking styles for the alert

.alert {
  background-color: red;
  border-radius: 8px;
  padding: 30px;
  color: white;
}

It's time to wire it up. In the first step we create a links function in the component directly before the alert interface. JavaScript users can simply drop the type.

import { LinksFunction } from 'remix';
import styles from './alert.css';

export const links: LinksFunction = () => [{ rel: 'stylesheet', href: styles }];

Integrate the component in the parent

Let's say we want to use the component in our root.tsx file for some reason. First step is to import the component and the links function.

import { Alert, links as alertLinks } from '~/components/alert/alert';

and add the alertLinks to the parent components links function as follows

export const links: LinksFunction = () => [
  ...alertLinks(),
  { rel: 'stylesheet', href: globalStyles },
];

There can be other style imports as usual. This works for root.tsx, for other parent custom components that live inside the /components folder, or for routing components.

Conclusion

You can write your scoped styles and place it right next to your custom components. You need to import them and export the links function. When you import your custom component in the parent component (not necessarily a routing component), you import the links, too. The return value of the links function (the array of stylesheet objects), will be spreaded into links object of the parent component. In the end each route knows exactly which styles are used on the page and you have fine-grained control about when to use which styles. Since remix.run's focus is on server-side rendering this makes sense, because we only want to ship to the user what's needed. No page won't ship with styles that aren't used on it. The way of using CSS imports is also the recommended way in the official docs.