diff --git a/deno.lock b/deno.lock index 476e8585fe7..b0c99ebecd8 100644 --- a/deno.lock +++ b/deno.lock @@ -1,6 +1,7 @@ { "version": "5", "specifiers": { + "jsr:@astral/astral@0.5.6": "0.5.6", "jsr:@astral/astral@~0.5.6": "0.5.6", "jsr:@deno-library/progress@^1.5.1": "1.5.1", "jsr:@deno/cache-dir@0.14": "0.14.0", @@ -6110,6 +6111,17 @@ "npm:vite@^7.1.4" ] } + }, + "links": { + "npm:@deno/vite-plugin@2.0.2": { + "dependencies": [ + "npm:@jsr/deno__loader@0.5", + "npm:@jsr/std__jsonc@1" + ], + "peerDependencies": [ + "npm:vite@5 || 6 || 7 || 8" + ] + } } } } diff --git a/docs/latest/advanced/vite.md b/docs/latest/advanced/vite.md index cbabf0ff43f..dec529eb355 100644 --- a/docs/latest/advanced/vite.md +++ b/docs/latest/advanced/vite.md @@ -83,7 +83,8 @@ Behind the scenes, the Fresh Vite plugin: During development (`deno task dev`), the Fresh Vite plugin enables HMR so that changes to components, islands, and CSS are reflected in the browser instantly without a full page reload. This is powered by Prefresh, Preact's fast refresh -implementation. +implementation. See [Styling](/docs/concepts/styling) for details on how CSS is +handled. ## Debugging diff --git a/docs/latest/concepts/file-routing.md b/docs/latest/concepts/file-routing.md index 8bd576cca3b..582cb40c6d5 100644 --- a/docs/latest/concepts/file-routing.md +++ b/docs/latest/concepts/file-routing.md @@ -101,6 +101,9 @@ array. This is a top-level export, separate from `config`: export const css = ["./assets/dashboard.css"]; ``` +See [Styling](/docs/concepts/styling) for all CSS approaches including CSS +Modules, preprocessors, and global stylesheets. + ## Route Groups When working with [layouts](/docs/advanced/layouts) or diff --git a/docs/latest/concepts/islands.md b/docs/latest/concepts/islands.md index b2f88a7efbc..a0fa1cf177a 100644 --- a/docs/latest/concepts/islands.md +++ b/docs/latest/concepts/islands.md @@ -118,12 +118,44 @@ import OtherIsland from "../islands/other-island.tsx"; ; ``` -## Rendering islands on client only +## Styling islands -When using client-only APIs, like `EventSource` or `navigator.getUserMedia`, the -component would error during server-side rendering. Use the `IS_BROWSER` -constant from `fresh/runtime` to guard browser-only code. It is `false` on the -server and `true` in the browser: +Islands can import CSS files just like any other module. CSS Modules +(`*.module.css`) are the recommended approach for scoped styles. Fresh +automatically collects CSS from each island's module graph and injects it as +`` tags during server rendering, so styles are available before hydration. + +See [Styling](/docs/concepts/styling) for details on CSS Modules, global +stylesheets, and other approaches. + +## Client-only islands + +Some libraries (e.g. Monaco Editor, certain charting libraries) reference +browser globals like `document` at the module top level, which crashes during +server-side rendering. You can mark an island as **client-only** by adding +`export const clientOnly = true`. Fresh will skip executing the component on the +server and render an empty placeholder instead. On the client, the component +renders normally. + +```tsx islands/my-editor.tsx +export const clientOnly = true; + +export default function MyEditor() { + // Safe to use document, window, etc. — this code never runs on the server. + return
{/* ... */}
; +} +``` + +> [warn]: Client-only islands produce no meaningful HTML on the server. This +> means search engines will not see their content, and users will see an empty +> placeholder until JavaScript loads. Use this only when the component truly +> cannot run on the server. + +### Using `IS_BROWSER` for a custom fallback + +If the module itself can be loaded on the server but you only need to guard +certain API calls, use the `IS_BROWSER` constant from `fresh/runtime` instead. +This lets you return a meaningful SSR fallback: ```tsx islands/my-island.tsx import { IS_BROWSER } from "fresh/runtime"; diff --git a/docs/latest/concepts/static-files.md b/docs/latest/concepts/static-files.md index 4b587444e98..8f7a568111b 100644 --- a/docs/latest/concepts/static-files.md +++ b/docs/latest/concepts/static-files.md @@ -37,6 +37,9 @@ import "./assets/styles.css"; - Files **referenced by URL path** (favicon.ico, fonts, robots.txt, PDFs, etc.): place in `static/` +See [Styling](/docs/concepts/styling) for a complete guide to CSS handling in +Fresh. + > [tip]: Always use root-relative URLs (starting with `/`) when referencing > static files in HTML. For example, use `src="/image/photo.png"` instead of > `src="image/photo.png"`. Relative paths resolve against the browser's current diff --git a/docs/latest/concepts/styling.md b/docs/latest/concepts/styling.md new file mode 100644 index 00000000000..98cbbfbb4aa --- /dev/null +++ b/docs/latest/concepts/styling.md @@ -0,0 +1,253 @@ +--- +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](https://vite.dev/guide/features#css). 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 `` | Use the `` component | + +## Global stylesheets + +The most common pattern is importing a CSS file from your `client.ts` entry +point. This makes the styles available on every page. + +```css assets/styles.css +body { + font-family: system-ui, sans-serif; + line-height: 1.6; +} +``` + +```ts client.ts +import "./assets/styles.css"; +``` + +Vite processes this import, applies any configured PostCSS transforms, and: + +- In **development**: injects the CSS as inline `