🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Tailwind

Drive Tailwind v4 with its Node API. Mochi ships an opt-in helper at mochi-framework/tailwind that compiles your input CSS at server startup and re-runs on file changes in dev. Then import the generated file from any .svelte and Mochi’s CSS-import bundler links it scoped to the page.

Setup

  1. Install Tailwind alongside its Node and scanner packages:
bun add tailwindcss @tailwindcss/node @tailwindcss/oxide
  1. Write an input CSS that imports the layers you want and tells Tailwind where to scan:
/* file: src/styles/app.css */
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css';

@source './*.svelte';
  1. Call setupTailwind at module scope in src/index.ts, before Mochi.serve(). Top-level await ensures the generated CSS exists for both the build CLI and the dev server:
// file: src/index.ts
import { Mochi } from 'mochi-framework';
import { setupTailwind } from 'mochi-framework/tailwind';

await setupTailwind({
  input: './src/styles/app.css',
  output: './src/styles/app.generated.css',
  minify: process.env.NODE_ENV !== 'development',
});

await Mochi.serve({
  routes: {
    '/': Mochi.page('./src/Home.svelte'),
  },
});
  1. import the generated file from any .svelte that uses Tailwind classes:
<!-- file: src/Home.svelte -->
<script>
  import './styles/app.generated.css';
</script>

<button class="rounded-md bg-emerald-600 px-3 py-1.5 text-white hover:bg-emerald-700">Click</button>
  1. Add app.generated.css to .gitignore — it is a build artifact.

The bundler strips the import from the JS bundle and serves the CSS at /_mochi/import-css/<hash>.css. The <link> is added to every page that transitively imports it. Pages that do not reference it ship no Tailwind. See CSS imports.

setupTailwind options

OptionDefaultMeaning
inputPath to the input CSS (@imports and @source rules).
outputPath where the generated CSS is written, stable for import.
basedirectory of inputAnchors @source patterns.
minifyfalseMinify the optimised output. Set from process.env.NODE_ENV.

Dev rebuilds

In development, setupTailwind subscribes to file:change on mochiEvents and rebuilds on .svelte / .ts / .js / .html / .md / .svx / .css changes. The resulting write goes through Mochi’s CSS fast-path — a stylesheet swap, not a full SSR rebuild. The watcher attaches only when process.env.NODE_ENV === 'development'.

Production builds

setupTailwind static-imports @tailwindcss/oxide, a native module. If your production runtime image uses a different libc than the install image, the binding installed at build time fails to load at runtime and the server crashes at startup with Cannot find native binding.

Generate the CSS at build time and dynamic-import the helper so production never loads oxide:

// file: src/index.ts
if (process.env.NODE_ENV === 'development') {
  const { setupTailwind } = await import('mochi-framework/tailwind');
  await setupTailwind({
    input: './src/styles/app.css',
    output: './src/styles/app.generated.css',
  });
}

Pair it with a prebuild script that compiles the CSS ahead of mochi-framework build:

// file: scripts/prebuild.ts
import { compileTailwind } from 'mochi-framework/tailwind';

await compileTailwind({
  input: './src/styles/app.css',
  output: './src/styles/app.generated.css',
  minify: true,
});
"scripts": {
  "build": "bun scripts/prebuild.ts && mochi-framework build"
}

Preflight and resets

The example imports tailwindcss/utilities.css without layer(utilities). If your shell’s CSS ships an unlayered universal reset, wrapping utilities in layer(utilities) lets the unlayered reset clobber .p-6, .mt-2, and so on, because unlayered styles beat layered ones in the cascade.

The example also skips Tailwind’s preflight so the stylesheet does not reset unrelated UI. The cost is that user-agent defaults leak through, so <button> keeps its rounded macOS pill shape. Add a small reset:

/* file: src/styles/app.css */
button {
  appearance: none;
  background: transparent;
  border: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

To opt back in, @import 'tailwindcss/preflight.css' layer(base);.

See it in action

Live demos showing key concepts from this page