How toLoad Dynamic Stylesheets in React Router

In some cases, you may need to dynamically load styles based on user preferences or organizational settings. For example, an organization might customize the UI for its members, or a user might select a theme that requires loading a specific CSS file.

Generating CSS On-Demand

If the CSS needs to be generated dynamically—perhaps based on database-stored data—you must first create a route to serve the generated CSS:

resources/styles.ts
import { stylesheet } from "remix-utils/responses"; import type { Route } from "./+types/styles"; export async function loader({ request }: Route.LoaderArgs) { let user = await authenticate(request); if (!user) return stylesheet(`/* No user found, replace with fallback */`); let styles = await getStylesForUser(user); return stylesheet(styles); }

Then, in app/root.tsx, load the styles from the newly created route:

app/root.tsx
// some code here export function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="/styles.css" /> </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); } // more code here

Ensure the route is registered in app/routes.ts:

app/routes.ts
// some code here export default [ // some routes here route("styles.css", "resources/styles.ts"), // more routes here ] satisfies RouteConfig;

Now, when a user visits the page, the CSS file will be generated in real time and served dynamically.

Dynamically Load Static CSS Files

If you need to load one of several predefined CSS files based on a condition—such as a user’s theme preference—you can achieve this by combining a LoaderFunction with a MetaFunction:

app/root.tsx
export async function loader({ request }: Route.LoaderArgs) { let user = await authenticate(request); if (!user) return data({ user: null, theme: "default" }); return data({ user, theme: user.theme }); } export function meta({ data }: Route.MetaArgs) { // if root loader failed, always load default styles if (!data) { return [ { tagName: "link", rel: "stylesheet", href: "/themes/default.css" }, ]; } // if we have the loader data, load the user's theme or the fallback return [ { tagName: "link", rel: "stylesheet", href: `/themes/${data.theme}.css` }, ]; }

Create a set of CSS files in public/themes/, such as default.css, dark.css, and light.css, ensuring the appropriate stylesheet is loaded based on user preferences.

Generating CSS On-Demand with Dynamic URLs

If each user requires a unique CSS file with a URL like /styles/:userId.css, you can combine the previous approaches:

First, create a route to generate the CSS dynamically:

resources/styles.ts
import { stylesheet } from "remix-utils/responses"; import type { Route } from "./+types/styles"; export async function loader({ request, params }: Route.LoaderArgs) { let styles = await getStylesForUser(params.userId); return stylesheet(styles); }

Then, modify app/root.tsx to load the user-specific stylesheet:

app/root.tsx
export async function loader({ request }: Route.LoaderArgs) { let user = await authenticate(request); if (!user) return data({ user: null }); return data({ user }); } export function meta({ data }: Route.MetaArgs) { if (!data) return []; return [ { tagName: "link", rel: "stylesheet", href: `/styles/${data.user.id}.css` }, ]; }

Finally, register the dynamic route in app/routes.ts:

app/routes.ts
export default [ // some routes here route("styles/:userId.css", "resources/styles.ts"), // more routes here ] satisfies RouteConfig;

Now, when a user visits the page, the application will generate and serve a personalized CSS file dynamically.