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 Case | Approach | Why |
|---|---|---|
| CSS reset, custom properties | Global CSS in layout | Applied everywhere, no scoping needed |
| Typography, base styles | Global CSS in layout | Consistent across all pages |
| Component-specific styles | CSS Modules | Scoped, no naming collisions |
| Utility classes (Tailwind-style) | Global CSS or Tailwind | Utility 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.