| description | How to style your Fresh app: global stylesheets, Tailwind CSS, CSS Modules, route-scoped CSS, and preprocessors. |
|---|
Fresh supports several approaches to styling, all powered by Vite's CSS handling. Choose the approach that fits your use case:
| Goal | Approach |
|---|---|
| Global styles | Import CSS in client.ts |
| Utility-first CSS | Tailwind via @tailwindcss/vite |
| Scoped component styles | CSS Modules (*.module.css) |
| Route-specific styles | export const css or side-effect import |
| Preprocessors (SCSS, Less) | Install the npm package and import directly |
| Static stylesheets | Place in static/, reference by URL path |
Inline styles in <head> |
Use the <Head> component |
The most common pattern is importing a CSS file from your client.ts entry
point. This makes the styles available on every page.
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
}import "./assets/styles.css";Vite processes this import, applies any configured PostCSS transforms, and:
- In development: injects the CSS as inline
<style>tags with hot module replacement. - In production: extracts the CSS to a hashed
.cssfile served with long-lived cache headers.
[info]: Place imported CSS files outside the
static/directory (e.g. inassets/). Files instatic/are served as-is and would be duplicated in the build output. See Static files for details.
Fresh works with Tailwind CSS via the official
@tailwindcss/vite plugin:
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [fresh(), tailwindcss()],
});@import "tailwindcss";import "./assets/styles.css";Then use Tailwind classes in your components:
export default function Home() {
return <h1 class="text-4xl font-bold text-blue-600">Hello, Fresh!</h1>;
}CSS Modules scope class names to
the component that imports them, preventing naming collisions. Any file ending in
.module.css is treated as a CSS Module by Vite.
.counter {
display: flex;
gap: 0.5rem;
align-items: center;
}
.count {
font-variant-numeric: tabular-nums;
min-width: 3ch;
text-align: center;
}import { useSignal } from "@preact/signals";
import styles from "./Counter.module.css";
export default function Counter() {
const count = useSignal(0);
return (
<div class={styles.counter}>
<button onClick={() => count.value--}>-</button>
<span class={styles.count}>{count}</span>
<button onClick={() => count.value++}>+</button>
</div>
);
}CSS Modules work in islands, server-only components, and route files. Fresh
automatically collects the CSS for any island and injects it as a <link> tag
during server rendering.
Deno's type checker does not natively understand *.module.css imports. Add
vite/client to your deno.json to pick up Vite's ambient types (which cover
CSS Modules, ?url/?raw imports, and import.meta.env):
This declares *.module.css imports as Record<string, string>, which gives
you autocompletion and type safety for class name lookups. Projects scaffolded
with deno run -A jsr:@fresh/init already include this.
You can load CSS for a specific route in two ways.
Import a CSS file directly in the route module:
import "./dashboard.css";
export default function Dashboard() {
return <main class="dashboard">...</main>;
}Export a css array with paths to CSS files:
export const css = ["./assets/dashboard.css"];
export default function Dashboard() {
return <main class="dashboard">...</main>;
}Both approaches scope the CSS to the route — the styles are only loaded when that route is rendered. See File routing for more on route exports.
Since Fresh uses Vite, you can use CSS preprocessors by installing the corresponding npm package. No additional Vite plugin is needed.
deno install npm:sass$primary: #3b82f6;
.btn-primary {
background-color: $primary;
&:hover {
background-color: darken($primary, 10%);
}
}import "./assets/theme.scss";deno install npm:less@primary: #3b82f6;
.btn-primary {
background-color: @primary;
}import "./assets/theme.less";Preprocessor files also work with CSS Modules (e.g. Button.module.scss) and
route-scoped imports.
For CSS files that should be served without processing, place them in the
static/ directory and reference them by URL path:
import { Head } from "fresh/runtime";
export default function Home() {
return (
<>
<Head>
<link rel="stylesheet" href="/legacy.css" />
</Head>
<h1>Hello</h1>
</>
);
}Static files are served with
ETag
headers for caching. Use asset() for cache-busted URLs with a one-year cache
lifetime.
When an island imports CSS (via CSS Modules, side-effect imports, or preprocessors), Fresh handles it automatically:
- During the build, Vite extracts CSS from each island's module graph into
separate hashed
.cssfiles. - At runtime, when the server renders an island, Fresh looks up its associated CSS and adds it to the page.
- The CSS is injected as
<link>tags in<head>so styles are available before the island hydrates.
In development mode, CSS is injected as inline <style> tags with hot module
replacement — changes are reflected instantly without a page reload.
{ "compilerOptions": { "types": ["vite/client"] } }