Monsoon v0.2.0
Made by Alexandre Kohler Star

Installation

Install Monsoon in SvelteKit

SvelteKit has a natural home for each piece: the stylesheet import in src/routes/+layout.svelte, and the theme script in src/app.html, which is served before any JavaScript runs.

2 min readSvelteKit

Install

src/app.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css" />
    <script>(function(){var t;try{t=localStorage.getItem('monsoon-theme')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.add(d?'dark':'light')})()</script>
    %sveltekit.head%
  </head>
  <body data-sveltekit-preload-data="hover" class="bg-background text-foreground antialiased">
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>

Use the classes

src/lib/components/PlanCard.svelte
<script>
  let { plan } = $props();
</script>

<article
  class={plan.featured
    ? 'rounded-2xl border bg-surface p-6 space-y-4 ring-2 ring-ring'
    : 'rounded-2xl border bg-surface p-6 space-y-4 shadow-sm'}
>
  <h2 class="text-lg font-semibold">{plan.name}</h2>
  <p class="text-3xl font-bold">{plan.price}</p>
  <a
    href="/signup?plan={plan.slug}"
    class="block rounded-lg px-4 py-2 text-center font-medium"
    class:bg-primary={plan.featured}
    class:text-primary-foreground={plan.featured}
    class:bg-muted={!plan.featured}
  >
    Choose {plan.name}
  </a>
</article>
src/routes/pricing/+page.svelte
<script>
  import PlanCard from '$lib/components/PlanCard.svelte';

  let { data } = $props();
</script>

<main class="mx-auto max-w-5xl px-6 py-12">
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
    {#each data.plans as plan (plan.slug)}
      <PlanCard {plan} />
    {/each}
  </div>
</main>

Dark mode

Components also render on the server, where document does not exist. Read the class in onMount and keep the server label neutral:

src/lib/components/ThemeToggle.svelte
<script>
  import { onMount } from 'svelte';

  let isDark = $state(false);

  onMount(() => {
    isDark = document.documentElement.classList.contains('dark');
  });

  function toggle() {
    isDark = !isDark;
    const theme = isDark ? 'dark' : 'light';
    const root = document.documentElement;
    root.classList.remove('dark', 'light');
    root.classList.add(theme);
    try { localStorage.setItem('monsoon-theme', theme); } catch (e) {}
  }
</script>

<button
  type="button"
  onclick={toggle}
  aria-pressed={isDark}
  class="rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
  {isDark ? 'Light mode' : 'Dark mode'}
</button>

The head script lives in src/app.html rather than in <svelte:head>, because app.html is plain HTML that runs before hydration. If you enable kit.csp, add the hash of that inline script to script-src.

Theme with --seed

Load the brand color on the server and apply it with the style: directive, so the first response is already themed:

src/routes/+layout.server.js
export async function load({ locals }) {
  return { brandColor: locals.team?.brandColor ?? '#0d9488' };
}
src/routes/+layout.svelte
<script>
  import 'monsooncss';

  let { data, children } = $props();
</script>

<div class="min-h-dvh bg-background" style:--seed={data.brandColor}>
  {@render children()}
</div>

Every semantic token (primary, secondary, accent, surfaces, muted text, borders and the focus ring) is derived from --seed in the browser. Theming lists the tokens.

Using the purge CLI

package.json
{
  "scripts": {
    "dev": "vite dev",
    "css:purge": "monsooncss purge \"src/**/*.{svelte,html,js,ts}\" -o src/lib/monsoon.purged.css",
    "build": "npm run css:purge && vite build",
    "preview": "vite preview"
  }
}
vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { fileURLToPath } from 'node:url';

const purged = fileURLToPath(new URL('./src/lib/monsoon.purged.css', import.meta.url));

export default defineConfig(({ command }) => ({
  plugins: [sveltekit()],
  resolve: {
    alias: command === 'build' ? { monsooncss: purged } : {},
  },
}));

import 'monsooncss' in the layout now loads the full file under vite dev and the purged file in builds. With the CDN setup, write to static/monsoon.css and change the <link> in src/app.html to %sveltekit.assets%/monsoon.css.

Classes used only in class: directives, such as class:bg-muted={...} above, are not detected by the purge CLI. Put them in a quoted string elsewhere or add them with --safelist bg-muted.

The purge CLI reads your files as plain text and keeps every class name it can see. It cannot see a name that is assembled at runtime, so write each class in full: isActive ? 'bg-primary' : 'bg-muted' survives a purge, 'bg-' + color does not. Names that only exist at runtime (from a database, a CMS or an API) go in --safelist bg-success,bg-danger.

The purged file always keeps the reset, the theme tokens, the .dark and .light scopes and the keyframes, so dark mode and --seed keep working. See the purge CLI reference for every option.

Next steps

Frequently asked questions

Why not import Monsoon in src/app.html?

app.html is a template, not a module, so it cannot import from node_modules. Use the root layout for the import and app.html for the CDN link and the head script.

Does it work with prerendering and adapter-static?

Yes. The stylesheet and the head script end up in every prerendered page.

No recent searches

Try , , or .

Recent

Loading the search index

No results for ""

Check the spelling, or search a class name such as px-4.