Viewing docs for an older release. View latest
Styling
On this page

Styling

The primary way to style in Remix (and the web) is to add a <link rel="stylesheet"> to the page. In Remix, you can add these links via the Route Module links export at route layout boundaries. When the route is active, the stylesheet is added to the page. When the route is no longer active, the stylesheet is removed.

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: "https://unpkg.com/modern-css-reset@1.4.0/dist/reset.min.css",
    },
  ];
};

Each nested route's links are merged (parents first) and rendered as <link> tags by the <Links/> you rendered in app/root.js in the head of the document.

import { Links } from "@remix-run/react";
// ...
export default function Root() {
  return (
    <html>
      <head>
        <Links />
        {/* ... */}
      </head>
      {/* ... */}
    </html>
  );
}

You can also import CSS files directly into your modules and Remix will:

  1. Copy the file to your browser build directory
  2. Fingerprint the file for long-term caching
  3. Return the public URL to your module to be used while rendering
// ...
import styles from "~/styles/global.css";
// styles is now something like /build/global-AE33KB2.css

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

Remix also has built-in support for the following:

CSS Ecosystem and Performance

In general, stylesheets added to the page with <link> tend to provide the best user experience:

  • The URL is cacheable in browsers and CDNs
  • The URL can be shared across pages in the app
  • The stylesheet can be loaded in parallel with the JavaScript bundles
  • Remix can prefetch CSS assets when the user is about to visit a page with <Link rel="prefetch">.
  • Changes to components don't break the cache for the styles
  • Changes to the styles don't break the cache for the JavaScript

Remix also supports "runtime" frameworks like styled components where styles are evaluated at runtime but don't require any kind of bundler integration--though we would prefer your stylesheets had a URL instead of being injected into style tags.

The two most popular approaches in the Remix community are route-based stylesheets and Tailwind. Both have exceptional performance characteristics. In this document, we'll show how to use these two approaches as well as a few more.

Regular Stylesheets

Remix makes writing plain CSS a viable option even for apps with a lot of UI. In our experience, writing plain CSS had maintenance issues for a few reasons. It was difficult to know:

  • how and when to load CSS, so it was usually all loaded on every page
  • if the class names and selectors you were using were accidentally styling other UI in the app
  • if some rules weren't even used anymore as the CSS source code grew over time

Remix alleviates these issues with route-based stylesheets. Nested routes can each add their own stylesheets to the page and Remix will automatically prefetch, load, and unload them with the route. When the scope of concern is limited to just the active routes, the risks of these problems are reduced significantly. The only chances for conflicts are with the parent routes' styles (and even then, you will likely see the conflict since the parent route is also rendering).

Route Styles

Each route can add style links to the page, for example:

import styles from "~/styles/dashboard.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}
import styles from "~/styles/accounts.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}
import styles from "~/styles/sales.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

Given these routes, this table shows which CSS will apply at specific URLs:

URL Stylesheets
/dashboard - dashboard.css
/dashboard/accounts - dashboard.css
- accounts.css
/dashboard/sales - dashboard.css
- sales.css

It's subtle, but this little feature removes a lot of the difficulty when styling your app with plain stylesheets.

Shared Component Styles

Websites large and small usually have a set of shared components used throughout the rest of the app: buttons, form elements, layouts, etc. When using plain style sheets in Remix there are two approaches we recommend.

Shared stylesheet

The first approach is very simple. Put them all in a shared.css file included in app/root.tsx. That makes it easy for the components themselves to share CSS code (and your editor to provide intellisense for things like custom properties), and each component already needs a unique module name in JavaScript anyway, so you can scope the styles to a unique class name or data attribute:

/* scope with class names */
.PrimaryButton {
  /* ... */
}

.TileGrid {
  /* ... */
}

/* or scope with data attributes to avoid concatenating
   className props, but it's really up to you */
[data-primary-button] {
  /* ... */
}

[data-tile-grid] {
  /* ... */
}

While this file may become large, it'll be at a single URL that will be shared by all routes in the app.

This also makes it easy for routes to adjust the styles of a component without needing to add an official new variant to the API of that component. You know it won't affect the component anywhere but the /accounts routes.

.PrimaryButton {
  background: blue;
}

Surfacing Styles

A second approach is to write individual css files per component and then "surface" the styles up to the routes that use them.

Perhaps you have a <Button> in app/components/button/index.js with styles at app/components/button/styles.css as well as a <PrimaryButton> that extends it.

Note that these are not routes, but they export links functions as if they were. We'll use this to surface their styles to the routes that use them.

[data-button] {
  border: solid 1px;
  background: white;
  color: #454545;
}
import styles from "./styles.css";

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

export const Button = React.forwardRef(
  ({ children, ...props }, ref) => {
    return <button {...props} ref={ref} data-button />;
  }
);
Button.displayName = "Button";

And then a <PrimaryButton> that extends it:

[data-primary-button] {
  background: blue;
  color: white;
}
import { Button, links as buttonLinks } from "../button";

import styles from "./styles.css";

export const links = () => [
  ...buttonLinks(),
  { rel: "stylesheet", href: styles },
];

export const PrimaryButton = React.forwardRef(
  ({ children, ...props }, ref) => {
    return (
      <Button {...props} ref={ref} data-primary-button />
    );
  }
);
PrimaryButton.displayName = "PrimaryButton";

Note that the primary button's links include the base button's links. This way consumers of <PrimaryButton> don't need to know its dependencies (just like JavaScript imports).

Because these buttons are not routes, and therefore not associated with a URL segment, Remix doesn't know when to prefetch, load, or unload the styles. We need to "surface" the links up to the routes that use the components.

Consider that routes/index.js uses the primary button component:

import {
  PrimaryButton,
  links as primaryButtonLinks,
} from "~/components/primary-button";
import styles from "~/styles/index.css";

export function links() {
  return [
    ...primaryButtonLinks(),
    { rel: "stylesheet", href: styles },
  ];
}

Now Remix can prefetch, load, and unload the styles for button.css, primary-button.css, and the route's index.css.

An initial reaction to this is that routes have to know more than you want them to. Keep in mind that each component must be imported already, so it's not introducing a new dependency, just some boilerplate to get the assets. For example, consider a product category page like this:

import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";

import { AddFavoriteButton } from "~/components/add-favorite-button";
import { ProductDetails } from "~/components/product-details";
import { ProductTile } from "~/components/product-tile";
import { TileGrid } from "~/components/tile-grid";
import styles from "~/styles/$category.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

export async function loader({ params }: LoaderArgs) {
  return json(
    await getProductsForCategory(params.category)
  );
}

export default function Category() {
  const products = useLoaderData<typeof loader>();
  return (
    <TileGrid>
      {products.map((product) => (
        <ProductTile key={product.id}>
          <ProductDetails product={product} />
          <AddFavoriteButton id={product.id} />
        </ProductTile>
      ))}
    </TileGrid>
  );
}

The component imports are already there, we just need to surface the assets:

import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

import {
  AddFavoriteButton,
  links as addFavoriteLinks,
} from "~/components/add-favorite-button";
import {
  ProductDetails,
  links as productDetailsLinks,
} from "~/components/product-details";
import {
  ProductTile,
  links as productTileLinks,
} from "~/components/product-tile";
import {
  TileGrid,
  links as tileGridLinks,
} from "~/components/tile-grid";
import styles from "~/styles/$category.css";

export const links: LinksFunction = () => {
  return [
    ...tileGridLinks(),
    ...productTileLinks(),
    ...productDetailsLinks(),
    ...addFavoriteLinks(),
    { rel: "stylesheet", href: styles },
  ];
};

// ...

While that's a bit of boilerplate it enables a lot:

  • You control your network tab, and CSS dependencies are clear in the code
  • Co-located styles with your components
  • The only CSS ever loaded is the CSS that's used on the current page
  • When your components aren't used by a route, their CSS is unloaded from the page
  • Remix will prefetch the CSS for the next page with <Link prefetch>
  • When one component's styles change, browser and CDN caches for the other components won't break because they are all have their own URLs.
  • When a component's JavaScript changes but its styles don't, the cache is not broken for the styles

Asset Preloads

Since these are just <link> tags, you can do more than stylesheet links, like adding asset preloads for SVG icon backgrounds of your elements:

[data-copy-to-clipboard] {
  background: url("/icons/clipboard.svg");
}
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

import styles from "./styles.css";

export const links: LinksFunction = () => [
  {
    rel: "preload",
    href: "/icons/clipboard.svg",
    as: "image",
    type: "image/svg+xml",
  },
  { rel: "stylesheet", href: styles },
];

export const CopyToClipboard = React.forwardRef(
  ({ children, ...props }, ref) => {
    return (
      <Button {...props} ref={ref} data-copy-to-clipboard />
    );
  }
);
CopyToClipboard.displayName = "CopyToClipboard";

Not only will this make the asset high priority in the network tab, but Remix will turn that preload into a prefetch when you link to the page with <Link prefetch>, so the SVG background is prefetched, in parallel, with the next route's data, modules, stylesheets, and any other preloads.

Using plain stylesheets and <link> tags also opens up the ability to decrease the amount of CSS your user's browser has to process when it paints the screen. Link tags support media, so you can do the following:

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: mainStyles,
    },
    {
      rel: "stylesheet",
      href: largeStyles,
      media: "(min-width: 1024px)",
    },
    {
      rel: "stylesheet",
      href: xlStyles,
      media: "(min-width: 1280px)",
    },
    {
      rel: "stylesheet",
      href: darkStyles,
      media: "(prefers-color-scheme: dark)",
    },
  ];
};

Tailwind CSS

Perhaps the most popular way to style a Remix application in the community is to use Tailwind CSS. It has the benefits of inline-style collocation for developer ergonomics and is able to generate a CSS file for Remix to import. The generated CSS file generally caps out around 8-10kb, even for large applications. Load that file into the root.tsx links and be done with it. If you don't have any CSS opinions, this is a great approach.

To use the built-in Tailwind support, first enable the tailwind option in remix.config.js:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  tailwind: true,
  // ...
};

Install Tailwind as a dev dependency:

npm install -D tailwindcss

Then initialize a config file:

npx tailwindcss init --ts

Now we can tell it which files to generate classes from:

import type { Config } from "tailwindcss";

export default {
  content: ["./app/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
} satisfies Config;

Then include the @tailwind directives in your CSS. For example, you could create a tailwind.css file at the root of your app:

@tailwind base;
@tailwind components;
@tailwind utilities;

Then add tailwind.css to your root route's links function:

import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

// ...

import styles from "./tailwind.css";

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

With this setup in place, you can also use Tailwind's functions and directives anywhere in your CSS. Note that Tailwind will warn that no utility classes were detected in your source files if you never used it before.

If you're also using Remix's built-in PostCSS support, the Tailwind PostCSS plugin will be automatically included if it's missing, but you can also choose to manually include the Tailwind plugin in your PostCSS config instead if you prefer.

If you're using VS Code, it's recommended you install the Tailwind IntelliSense extension for the best developer experience.

It's recommended that you avoid Tailwind's @import syntax (e.g. @import 'tailwindcss/base') in favor of Tailwind directives (e.g. @tailwind base). Tailwind replaces its import statements with inlined CSS but this can result in the interleaving of styles and import statements. This clashes with the restriction that all import statements must be at the start of the file. Alternatively, you can use PostCSS with the postcss-import plugin to process imports before passing them to esbuild.

Remote Stylesheets

You can load stylesheets from any server, here's an example of loading a modern css reset from unpkg.

import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: "https://unpkg.com/modern-css-reset@1.4.0/dist/reset.min.css",
    },
  ];
};

PostCSS

PostCSS is a popular tool with a rich plugin ecosystem, commonly used to prefix CSS for older browsers, transpile future CSS syntax, inline images, lint your styles and more. When a PostCSS config is detected, Remix will automatically run PostCSS across all CSS in your project.

For example, to use Autoprefixer, first enable the postcss option in remix.config.js:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  postcss: true,
  // ...
};

Install the PostCSS plugin.

npm install -D autoprefixer

Then add postcss.config.js in the Remix root referencing the plugin.

module.exports = {
  plugins: {
    autoprefixer: {},
  },
};

If you're using Vanilla Extract, since it's already playing the role of CSS preprocessor, you may want to apply a different set of PostCSS plugins relative to other styles. To support this, you can export a function from postcss.config.js which is given a context object that lets you know when Remix is processing a Vanilla Extract file.

module.exports = (ctx) => {
  return ctx.remix?.vanillaExtract
    ? {
        // PostCSS plugins for Vanilla Extract styles...
      }
    : {
        // PostCSS plugins for other styles...
      };
};

CSS Preprocessors

You can use CSS preprocessors like LESS and SASS. Doing so requires running an additional build process to convert these files to CSS files. This can be done via the command line tools provided by the preprocessor or any equivalent tool.

Once converted to CSS by the preprocessor, the generated CSS files can be imported into your components via the Route Module links export function, or included via side-effect imports when using CSS bundling, just like any other CSS file in Remix.

To ease development with CSS preprocessors you can add npm scripts to your package.json that generate CSS files from your SASS or LESS files. These scripts can be run in parallel alongside any other npm scripts that you run for developing a Remix application.

An example using SASS.

  1. First you'll need to install the tool your preprocess uses to generate CSS files.
npm add -D sass
  1. Add an npm script to your package.json's script section' that uses the installed tool to generate CSS files.
{
  // ...
  "scripts": {
    // ...
    "sass": "sass --watch app/:app/"
  }
  // ...
}

The above example assumes SASS files will be stored somewhere in the app folder.

The --watch flag included above will keep sass running as an active process, listening for changes to or for any new SASS files. When changes are made to the source file, sass will regenerate the CSS file automatically. Generated CSS files will be stored in the same location as their source files.

  1. Run the npm script.
npm run sass

This will start the sass process. Any new SASS files, or changes to existing SASS files, will be detected by the running process.

You might want to use something like concurrently to avoid needing two terminal tabs to generate your CSS files and also run remix dev.

npm add -D concurrently
{
  "scripts": {
    "dev": "concurrently \"npm run sass\" \"remix dev\""
  }
}

Running npm run dev will run the specified commands in parallel in a single terminal window.

CSS-in-JS libraries

You can use CSS-in-JS libraries like Styled Components. Some of them require a "double render" in order to extract the styles from the component tree during the server render. It's unlikely this will affect performance in a significant way; React is pretty fast.

Here's some sample code to show how you might use Styled Components with Remix (you can also find a runnable example in the Remix examples repository):

  1. First you'll need to put a placeholder in your root component to control where the styles are inserted.

    import type { MetaFunction } from "@remix-run/node"; // or cloudflare/deno
    import {
      Links,
      LiveReload,
      Meta,
      Outlet,
      Scripts,
      ScrollRestoration,
    } from "@remix-run/react";
    
    export const meta: MetaFunction = () => ({
      charset: "utf-8",
      viewport: "width=device-width, initial-scale=1",
    });
    
    export default function App() {
      return (
        <html lang="en">
          <head>
            <Meta />
            <Links />
            {typeof document === "undefined"
              ? "__STYLES__"
              : null}
          </head>
          <body>
            <Outlet />
            <ScrollRestoration />
            <Scripts />
            <LiveReload />
          </body>
        </html>
      );
    }
    
  2. Your entry.server.tsx will look something like this:

    import type {
      AppLoadContext,
      EntryContext,
    } from "@remix-run/node"; // or cloudflare/deno
    import { RemixServer } from "@remix-run/react";
    import { renderToString } from "react-dom/server";
    import { ServerStyleSheet } from "styled-components";
    
    export default function handleRequest(
      request: Request,
      responseStatusCode: number,
      responseHeaders: Headers,
      remixContext: EntryContext,
      loadContext: AppLoadContext
    ) {
      const sheet = new ServerStyleSheet();
    
      let markup = renderToString(
        sheet.collectStyles(
          <RemixServer
            context={remixContext}
            url={request.url}
          />
        )
      );
      const styles = sheet.getStyleTags();
      markup = markup.replace("__STYLES__", styles);
    
      responseHeaders.set("Content-Type", "text/html");
    
      return new Response("<!DOCTYPE html>" + markup, {
        status: responseStatusCode,
        headers: responseHeaders,
      });
    }
    

Other CSS-in-JS libraries will have a similar setup. If you've got a CSS framework working well with Remix, please contribute an example!

NOTE: You may run into hydration warnings when using Styled Components. Hopefully this issue will be fixed soon.

CSS Bundling

When using CSS-bundling features, you should avoid using export * due to an issue with esbuild's CSS tree shaking.

Many common approaches to CSS within the React community are only possible when bundling CSS, meaning that the CSS files you write during development are collected into a separate bundle as part of the build process.

When using CSS-bundling features, the Remix compiler will generate a single CSS file containing all bundled styles in your application. Note that any regular stylesheet imports will remain as separate files.

Unlike many other tools in the React ecosystem, we do not insert the CSS bundle into the page automatically. Instead, we ensure that you always have control over the link tags on your page. This lets you decide where the CSS file is loaded relative to other stylesheets in your app.

To get access to the CSS bundle, first install the @remix-run/css-bundle package.

npm install @remix-run/css-bundle

Then, import cssBundleHref and add it to a link descriptorโ€”most likely in root.tsx so that it applies to your entire application.

import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

export const links: LinksFunction = () => {
  return [
    ...(cssBundleHref
      ? [{ rel: "stylesheet", href: cssBundleHref }]
      : []),
    // ...
  ];
};

With this link tag inserted into the page, you're now ready to start using the various CSS bundling features built into Remix.

CSS Modules

To use the built-in CSS Modules support, first ensure you've set up CSS bundling in your application.

You can then opt into CSS Modules via the .module.css file name convention. For example:

.root {
  border: solid 1px;
  background: white;
  color: #454545;
}
import styles from "./styles.module.css";

export const Button = React.forwardRef(
  ({ children, ...props }, ref) => {
    return (
      <button
        {...props}
        ref={ref}
        className={styles.root}
      />
    );
  }
);
Button.displayName = "Button";

Vanilla Extract

Vanilla Extract is a zero-runtime CSS-in-TypeScript (or JavaScript) library that lets you use TypeScript as your CSS preprocessor. Styles are written in separate *.css.ts (or *.css.js) files and all code within them is executed during the build process rather than in your user's browser. If you want to keep your CSS bundle size to a minimum, Vanilla Extract also provides an official library called Sprinkles that lets you define a custom set of utility classes and a type-safe function for accessing them at runtime.

To use the built-in Vanilla Extract support, first ensure you've set up CSS bundling in your application.

Then, install Vanilla Extract's core styling package as a dev dependency.

npm install -D @vanilla-extract/css

You can then opt into Vanilla Extract via the .css.ts/.css.js file name convention. For example:

import { style } from "@vanilla-extract/css";

export const root = style({
  border: "solid 1px",
  background: "white",
  color: "#454545",
});
import * as styles from "./styles.css"; // Note that `.ts` is omitted here

export const Button = React.forwardRef(
  ({ children, ...props }, ref) => {
    return (
      <button
        {...props}
        ref={ref}
        className={styles.root}
      />
    );
  }
);
Button.displayName = "Button";

CSS Side-Effect Imports

Some NPM packages use side-effect imports of plain CSS files (e.g. import "./styles.css") to declare the CSS dependencies of JavaScript files. If you want to consume one of these packages, first ensure you've set up CSS bundling in your application.

Since JavaScript runtimes don't support importing CSS in this way, you'll need to add any relevant packages to the serverDependenciesToBundle option in your remix.config.js file. This ensures that any CSS imports are compiled out of your code before running it on the server. For example, to use React Spectrum:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverDependenciesToBundle: [
    /^@adobe\/react-spectrum/,
    /^@react-spectrum/,
    /^@spectrum-icons/,
  ],
  // ...
};
Docs and examples licensed under MIT