Skip to content

CSS and Styling

Every framework says CSS just works. Here is what actually happens in Juice, and why you should care about the difference between dev mode and production.

How CSS Works in Dev Mode

In development, Vite's module graph tracks CSS imports. When you write import './styles.css' in a component, Vite injects a <link>tag pointing to the dev server. Edit the CSS file and Vite replaces the stylesheet via HMR without a full page reload.

Juice adds one thing on top: the Vite plugin scans route and layout source files for CSS imports and makes sure the right <link> tags are injected for the current route. This means CSS imported in layout.tsx loads for every page, while CSS imported in blog/[slug].tsx loads only for blog pages.

How CSS Works in Production

On build, Vite bundles CSS into chunks. The Juice plugin writes the CSS mapping into the flight manifest under manifest.css, keyed by route pattern. Global CSS (imported by layouts) appears under '*'. The runtime injects <link rel="stylesheet"> tags into the HTML and uses bootstrapScriptContent to ensure styles load before the page paints.

Coming from Next.js: Next.js handles CSS similarly under the hood (Webpack extracts CSS chunks), but the mapping is opaque. In Juice, you can read the manifest to see exactly which CSS file serves which route. This matters when you are debugging why a style is not loading on a specific page.

Global CSS

Import a .css file in your root layout to apply it site-wide. Use global CSS for resets, CSS custom properties, typography defaults, and utility classes.

// app/routes/layout.tsx
import React from 'react';
import './global.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head><meta charSet="utf-8" /></head>
      <body>{children}</body>
    </html>
  );
}

CSS Modules

Name your file with .module.css and import it as an object. Class names are scoped automatically, so .card becomes something like .card_a1b2c3 at build time. No naming collisions.

/* app/components/card.module.css */
.card {
  border: 1px solid var(--border-color, #e2e8f0);
  border-radius: 8px;
  padding: 1.5rem;
}

.title {
  font-weight: 600;
  margin-bottom: 0.5rem;
}

.card:hover {
  border-color: var(--accent-color, #3b82f6);
}
// app/components/card.tsx
import styles from './card.module.css';

export function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className={styles.card}>
      <h3 className={styles.title}>{title}</h3>
      {children}
    </div>
  );
}

CSS Modules work in both server and client components. The scoped class names are resolved at build time, so there is no runtime overhead.

Preprocessors

Sass, Less, and Stylus work via Vite plugins. Install the preprocessor and import the file. No Juice-specific configuration needed.

bun add -d sass
// Then import .scss files directly
import './styles.scss';

// Or use Sass modules
import styles from './card.module.scss';

When to Use Global CSS vs Modules

Use CaseApproachWhy
CSS reset, custom propertiesGlobal CSS in layoutApplied everywhere, no scoping needed
Typography, base stylesGlobal CSS in layoutConsistent across all pages
Component-specific stylesCSS ModulesScoped, no naming collisions
Utility classes (Tailwind-style)Global CSS or TailwindUtility classes are inherently global

When NOT to Use Juice's CSS Pipeline

Tailwind CSS: If you are using Tailwind, you do not need CSS Modules. Tailwind's utility classes are global by design. Configure Tailwind via its Vite plugin and import the generated CSS in your root layout. The two approaches (Tailwind + Modules) can coexist but rarely should.

CSS-in-JS (styled-components, Emotion): These libraries work in client components but add JavaScript to the browser bundle. Server components cannot use runtime CSS-in-JS because there is no JavaScript execution in the browser for those components. If your team is invested in CSS-in-JS, be aware that you are opting out of one of the main benefits of server components (zero client JS).

Coming from Remix: Remix has a similar CSS import model backed by esbuild. The mechanics are nearly identical. If your Remix CSS setup works, it will work in Juice with minimal changes. The main difference is the manifest: Remix tracks CSS in its route manifest the same way, but Juice uses the Vite module graph instead of esbuild's metafile.