Monsoon v0.2.0
Made by Alexandre Kohler Star

Installation

Install Monsoon in Nuxt 3

Nuxt reads global styles and head tags from nuxt.config.ts, so the whole setup lives in one file. The same configuration works in Nuxt 4, where components and assets move under app/.

2 min readNuxt 3

Install

nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  app: {
    head: {
      link: [{ rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css' }],
      script: [{ innerHTML: "(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')})()" }],
    },
  },
});

The theme script goes in app.head.script, not in a plugin. Plugins run after the HTML has painted, which is too late to avoid a flash of the wrong scheme.

Use the classes

Components are auto imported, and classes work exactly as in any Vue template, with class for static names and :class for conditional ones:

components/PlanCard.vue
<script setup lang="ts">
defineProps<{ name: string; price: string; featured?: boolean }>();
</script>

<template>
  <article
    class="rounded-2xl border bg-surface p-6 space-y-4"
    :class="featured ? 'ring-2 ring-ring' : 'shadow-sm'"
  >
    <h2 class="text-lg font-semibold">{{ name }}</h2>
    <p class="text-3xl font-bold">{{ price }}</p>
    <NuxtLink
      to="/signup"
      class="block rounded-lg px-4 py-2 text-center font-medium"
      :class="featured ? 'bg-primary text-primary-foreground hover:bg-primary/90' : 'bg-secondary text-secondary-foreground hover:bg-secondary/80'"
    >
      Choose {{ name }}
    </NuxtLink>
  </article>
</template>
pages/pricing.vue
<template>
  <main class="mx-auto max-w-5xl px-6 py-12">
    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
      <PlanCard name="Starter" price="$0" />
      <PlanCard name="Team" price="$12" featured />
      <PlanCard name="Business" price="$40" />
    </div>
  </main>
</template>

Dark mode

On the server there is no localStorage, so the toggle reads the class only after mount. Server and client render the same first label, and hydration stays clean:

components/ThemeToggle.vue
<script setup lang="ts">
const isDark = ref(false);

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

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

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

Do not also set the class through useHead({ htmlAttrs: { class } }). Nuxt would then own that attribute and could remove the class the head script added.

Theme with --seed

Set the seed for the whole document from server data with useHead, so the first HTML is already themed:

app.vue
<script setup lang="ts">
const { data: team } = await useFetch('/api/team');

useHead({
  htmlAttrs: {
    style: () => '--seed: ' + (team.value?.brandColor ?? '#0d9488'),
  },
});
</script>

<template>
  <NuxtPage />
</template>

For a single component, bind it inline: <section :style="{ '--seed': color }">. Validate colors that come from users before you render them. 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": "nuxt dev",
    "css:purge": "monsooncss purge \"app.vue\" \"{components,layouts,pages}/**/*.{vue,ts}\" -o assets/css/monsoon.purged.css",
    "build": "npm run css:purge && nuxt build"
  }
}

Nuxt environment overrides pick the right file: the full package while you develop, the purged file when you build.

nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  $development: { css: ['monsooncss'] },
  $production: { css: ['~/assets/css/monsoon.purged.css'] },
  app: {
    head: {
      script: [{ innerHTML: "(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')})()" }],
    },
  },
});

In Nuxt 4 the source folder is app/, so scan "app/**/*.{vue,ts}" and write to app/assets/css/monsoon.purged.css (the ~ alias already points there).

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

Should I use the css array or app.head.link?

Use css when you install from npm: Nuxt bundles and fingerprints the file. Use app.head.link for the CDN, since there is nothing to bundle.

Does this work with static generation?

Yes. nuxt generate writes the same head script and stylesheet into every 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.