Skip to main content
Technology & EngineeringRemix228 lines

Styling

CSS strategies in Remix including route-scoped links, Tailwind CSS, CSS Modules, and CSS-in-JS approaches

Quick Summary30 lines
You are an expert in CSS strategies for Remix applications, including the `links` export, Tailwind CSS integration, CSS Modules, vanilla CSS, and scoping styles to routes.

## Key Points

- Use route-scoped `links` to avoid loading CSS for pages the user never visits — this keeps the critical CSS small.
- Prefer Tailwind or CSS Modules for component-level styles and reserve the `links` export for route-level sheets.
- Combine `links` arrays from child components using spread to bubble component CSS up to routes.
- Use `?url` suffix when importing CSS files with Vite-based Remix to get the URL string.
- Set up a consistent design token system (CSS custom properties or Tailwind theme) in global styles.
- Importing CSS directly without `?url` in Vite mode — this injects the CSS as a side effect instead of returning a URL string for the `links` export.
- Putting all styles in a single global stylesheet — defeats route-based code splitting for CSS.
- Using CSS-in-JS libraries that require client-side JavaScript — these break progressive enhancement and add JS bundle weight. Prefer compile-time solutions.
- Forgetting that `links` from child routes are only active when those routes are matched — styles disappear when navigating away.
- Not including component-level `links` in the route `links` export — the stylesheet never loads.

## Quick Example

```bash
npm install -D tailwindcss
npx tailwindcss init
```

```css
/* app/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
```
skilldb get remix-skills/StylingFull skill: 228 lines
Paste into your CLAUDE.md or agent config

Styling — Remix

You are an expert in CSS strategies for Remix applications, including the links export, Tailwind CSS integration, CSS Modules, vanilla CSS, and scoping styles to routes.

Overview

Remix takes a web-standards approach to styling. Each route can export a links function that returns <link> tags, which are added to the document <head> when the route is active and removed when it is not. This gives route-level CSS scoping without runtime overhead. Remix also supports Tailwind CSS, CSS Modules, PostCSS, and other strategies with zero or minimal configuration.

Core Concepts

Route-Scoped links Export

import type { LinksFunction } from "@remix-run/node";
import styles from "~/styles/dashboard.css?url";

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

export default function Dashboard() {
  return <div className="dashboard">...</div>;
}

When the user navigates away from /dashboard, the stylesheet is automatically removed from the document.

Global Styles

Add global stylesheets in root.tsx:

// app/root.tsx
import globalStyles from "~/styles/global.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: globalStyles },
  {
    rel: "preconnect",
    href: "https://fonts.googleapis.com",
  },
  {
    rel: "stylesheet",
    href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap",
  },
];

Implementation Patterns

Tailwind CSS

Remix has built-in Tailwind support. Install and configure it:

npm install -D tailwindcss
npx tailwindcss init
// tailwind.config.ts
import type { Config } from "tailwindcss";

export default {
  content: ["./app/**/*.{ts,tsx,jsx,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
} satisfies Config;
/* app/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
// app/root.tsx
import tailwind from "~/tailwind.css?url";

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

CSS Modules

Remix supports CSS Modules out of the box with Vite:

/* app/components/Button.module.css */
.button {
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  font-weight: 600;
}

.primary {
  background-color: #2563eb;
  color: white;
}

.secondary {
  background-color: #e5e7eb;
  color: #1f2937;
}
// app/components/Button.tsx
import styles from "./Button.module.css";

interface ButtonProps {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
}

export function Button({ variant = "primary", children }: ButtonProps) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
}

PostCSS

Create a postcss.config.js at the project root — Remix detects it automatically:

// postcss.config.js
export default {
  plugins: {
    "postcss-preset-env": {
      stage: 2,
      features: {
        "nesting-rules": true,
      },
    },
    autoprefixer: {},
  },
};

Responsive and Dark Mode with Tailwind

export default function Card({ title, description }: CardProps) {
  return (
    <div className="rounded-lg bg-white p-6 shadow-md dark:bg-gray-800">
      <h2 className="text-lg font-semibold text-gray-900 dark:text-white md:text-xl">
        {title}
      </h2>
      <p className="mt-2 text-sm text-gray-600 dark:text-gray-300">
        {description}
      </p>
    </div>
  );
}

Shared Component Styles with surfaceLinks

When a component has its own CSS, the consuming route must include it in its links export:

// app/components/Dialog.tsx
import dialogStyles from "./Dialog.css?url";

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

export function Dialog({ children }: { children: React.ReactNode }) {
  return <div className="dialog">{children}</div>;
}
// app/routes/settings.tsx
import { dialogLinks } from "~/components/Dialog";

export const links: LinksFunction = () => [...dialogLinks()];

Core Philosophy

Remix's styling approach reflects its commitment to web standards over framework-specific abstractions. The links export maps directly to HTML <link> tags, which is the platform's native mechanism for loading stylesheets. When a route is active, its stylesheets are present in the document head. When the user navigates away, the stylesheets are removed. This gives you route-level CSS code splitting without a runtime CSS-in-JS library or build-time extraction tool.

This model treats CSS as a co-located, route-scoped concern. Each route declares its own stylesheet dependencies, and Remix manages their lifecycle. The developer does not worry about global CSS conflicts because route-specific styles are loaded and unloaded as the user navigates. This is fundamentally different from a single global stylesheet that grows forever or a CSS-in-JS library that generates styles at runtime.

Remix is intentionally unopinionated about which CSS methodology you use within this framework. Tailwind, CSS Modules, vanilla CSS, or PostCSS all work without special Remix configuration. The links export handles the loading and unloading; the styling approach within those files is entirely your choice. This flexibility means you can adopt Remix without committing to a specific CSS paradigm, and you can change your approach later without framework-level migration.

Anti-Patterns

  • Putting all styles in a single global stylesheet. A monolithic stylesheet loads all CSS for the entire application on every page, defeating route-based code splitting. Use route-scoped links to load only the CSS needed for the current page.

  • Using CSS-in-JS libraries that require client-side JavaScript. Libraries like styled-components or Emotion add JavaScript to the client bundle, break progressive enhancement, and increase time-to-interactive. Prefer compile-time solutions (Tailwind, CSS Modules, PostCSS) that produce plain CSS files.

  • Importing CSS files without ?url in Vite mode. Without the ?url suffix, Vite injects the CSS as a side effect rather than returning a URL string. The links export expects a URL, not a side-effect import.

  • Forgetting to include component-level links in the route's links export. If a component declares its own links function but the consuming route does not spread those links into its own links export, the component's stylesheet never loads.

  • Using inline styles for complex layouts. Inline styles bypass the route-scoped loading model and cannot leverage PostCSS transformations, media queries, or pseudo-selectors. Reserve inline styles for truly dynamic, element-level values and use stylesheets for everything else.

Best Practices

  • Use route-scoped links to avoid loading CSS for pages the user never visits — this keeps the critical CSS small.
  • Prefer Tailwind or CSS Modules for component-level styles and reserve the links export for route-level sheets.
  • Combine links arrays from child components using spread to bubble component CSS up to routes.
  • Use ?url suffix when importing CSS files with Vite-based Remix to get the URL string.
  • Set up a consistent design token system (CSS custom properties or Tailwind theme) in global styles.

Common Pitfalls

  • Importing CSS directly without ?url in Vite mode — this injects the CSS as a side effect instead of returning a URL string for the links export.
  • Putting all styles in a single global stylesheet — defeats route-based code splitting for CSS.
  • Using CSS-in-JS libraries that require client-side JavaScript — these break progressive enhancement and add JS bundle weight. Prefer compile-time solutions.
  • Forgetting that links from child routes are only active when those routes are matched — styles disappear when navigating away.
  • Not including component-level links in the route links export — the stylesheet never loads.

Install this skill directly: skilldb add remix-skills

Get CLI access →