---
title: UnoCSS
description: Configure the UnoCSS preset, file scanning pipelines, variant
  groups, and semantic color variable generation.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# UnoCSS

> Configure the UnoCSS preset, file scanning pipelines, variant groups, and semantic color variable generation.

Moraine provides first-class support for UnoCSS through the `presetMoraine()` preset exported from `moraine/unocss`. It maps semantic colors, border radiuses, shadows, animation keyframes, z-index utilities, and state variants directly into UnoCSS.

## Installation and config

In an existing Solid/Vite project, install Moraine and the UnoCSS integration:

```shell
pnpm add moraine
pnpm add -D @subf/unocss
```

Add `presetMoraine()` alongside your preferred Wind preset (`presetWind4` or `presetWind3`). Wind4 is the default; set `wind3: true` for Wind3 compatibility. The preset does not detect the installed Wind preset automatically. Explicitly scan application files and Moraine's published classes:

```ts title="unocss.config.ts (Wind4)" group-id="unocss" {2,5,10,13}
import { defineConfig, presetWind4 } from '@subf/unocss'
import { presetMoraine } from 'moraine/unocss'

export default defineConfig({
  presets: [presetWind4(), presetMoraine()],
  content: {
    filesystem: [
      './index.html',
      './src/**/*.{js,ts,jsx,tsx}',
      './node_modules/moraine/dist/**/*.{mjs,jsx}',
    ],
    pipeline: {
      include: [/\.(?:mjs|js|ts|jsx|tsx|mdx?|html)(?:\?|$)/],
    },
  },
})
```

```ts title="unocss.config.ts (Wind3)" group-id="unocss" {2,5,10,13}
import { defineConfig, presetWind3 } from '@subf/unocss'
import { presetMoraine } from 'moraine/unocss'

export default defineConfig({
  presets: [presetWind3(), presetMoraine({ wind3: true })],
  content: {
    filesystem: [
      './index.html',
      './src/**/*.{js,ts,jsx,tsx}',
      './node_modules/moraine/dist/**/*.{mjs,jsx}',
    ],
    pipeline: {
      include: [/\.(?:mjs|js|ts|jsx|tsx|mdx?|html)(?:\?|$)/],
    },
  },
})
```

### Solid and Vite integration

Place `unocss.config.ts` and `vite.config.ts` in the application root. Register UnoCSS before Solid's plugin:

```ts title="vite.config.ts"
import UnoCSS from '@subf/unocss/vite'
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'

export default defineConfig({
  plugins: [UnoCSS(), solid()],
})
```

Import the generated CSS once in the application entry, so it is available on every route:

```tsx title="src/main.tsx"
import { render } from 'solid-js/web'

import { App } from './App'
import 'virtual:uno.css'

render(() => <App />, document.getElementById('app')!)
```

The HTML entry must have a matching mount element and load `src/main.tsx`:

```html title="index.html (body)"
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
```

Use the `App` from the [installation example](https://moraine.subf.dev/docs/installation.md#3-verify-the-installation) to check the button's appearance. No `MoraineProvider` is required for the preset's default theme. If you use TypeScript, include `vite/client` in your compiler types to recognize the virtual CSS import.

### Scanning component classes

Scan the whole `moraine/dist` directory with the glob above. The preset does not configure scanning paths for your application.

Filesystem scanning reads these classes independently of which modules have been requested through Vite. Keep both the application and package globs, including lazy-loaded route files and `.ts` files containing class maps. The `pipeline.include` filter also applies to filesystem extraction in `@subf/unocss/vite`; UnoCSS's default filter omits `.mjs` and `.ts`. Keep the extensions your app uses when changing this filter because it replaces the default. See [UnoCSS's extraction guide](https://unocss.dev/guide/extracting).

Filesystem globs are relative to Vite's root. The examples assume `src/` and `node_modules/moraine` live under that root. If you set a different Vite root, keep routes outside `src/`, or use a workspace with Moraine installed at the repository root, adjust the globs to match those locations. Add your Markdown or MDX directories when they contain application classes.

### Verification and troubleshooting

Verify the button from the installation guide on a clean dev-server start, after a restart with Vite's dependency cache retained, when directly opening a lazy-loaded route, and in a production preview. Check its background, padding, and border radius; a click handler working does not verify CSS generation.

If styles are missing:

1. Confirm `virtual:uno.css` is imported in the application entry and UnoCSS runs before the Solid plugin.
2. Confirm both the Wind preset and `presetMoraine()` are enabled. With Wind3, pass `wind3: true`.
3. Check that the filesystem globs match the installed Moraine distribution and all application source directories, including lazy routes.
4. Check that `pipeline.include` allows those file extensions and that custom exclusion filters do not reject the files.
5. Inspect a missing class in browser DevTools. A missing CSS rule points to extraction or utility configuration; an existing rule with an unresolved `--primary` or another theme variable points to theme configuration. With `themeDefaults: false`, the application must provide its theme variables.

For a suspected dependency-cache issue, stop the server and run your Vite dev command with `--force` (for a `dev` script that runs Vite, `pnpm run dev --force`). Alternatively, remove Vite's configured `cacheDir`, which defaults to `node_modules/.vite`, before restarting. Disable browser caching temporarily while diagnosing cached dependency requests. See [Vite's dependency caching guide](https://vite.dev/guide/dep-pre-bundling#caching).

After correcting the scan configuration, restart again with the cache retained. Clearing the cache is a diagnostic step, not a required part of normal startup. An improvement after `--force` alone does not establish a Moraine runtime bug. Restart after upgrading Moraine so its published files are scanned again.

Keep the CSS import in the application entry when using lazy routes; do not rely on visiting another route to load styles. Filesystem scanning can discover static classes before the route is loaded, but neither it nor pipeline extraction can infer arbitrary runtime strings such as `bg-${color}`. Use complete static class maps or an explicit UnoCSS safelist for application-generated classes.

## Preset options

`presetMoraine()` provides Moraine's utility tokens, state variants, opaque neutral light and dark colors as RGB values, a translucent backdrop, default shadows, and base values for radius, font size, and spacing. Dark `border` and `input` use `rgb(35, 35, 35)` and `rgb(47, 47, 47)`; their colors stay the same across parent surfaces. The default selectors are `:root` and `.dark`. Configure single-value tokens at the top level; put colors, shadows, and `colorScheme` in named `override` entries.

Semantic shadow defaults use the Tailwind values inherited by the [shadcn/ui neutral theme](https://ui.shadcn.com/docs/theming#default-theme-css): surface → `sm`, overlay → `md`, input → `xs`. The preset emits `--shadow-surface`, `--shadow-overlay`, and `--shadow-input` at `:root`; dark mode inherits these values unless `override.dark.shadows` supplies replacements. Native size utilities such as `shadow-xs` retain the CSS engine's own values and configuration. Set `themeDefaults: false` to omit default colors and semantic shadows while retaining explicit overrides.

```ts
presetMoraine({
  radius: '0.75rem',
  sidebarWidth: '18rem',
  override: {
    light: {
      colorScheme: 'light',
      shadows: {
        surface: '0 1px 2px rgba(0, 0, 0, 0.05)',
        overlay: '0 8px 24px rgba(0, 0, 0, 0.12)',
        input: 'none',
      },
      colors: {
        primary: { base: '#2563eb', foreground: '#fff' },
        control: '#fff',
        backdrop: 'rgb(0 0 0 / 0.2)',
      },
    },
    dark: {
      colorScheme: 'dark',
      colors: {
        primary: { base: '#60a5fa', foreground: '#172554', hover: '#93c5fd' },
      },
    },
    brand: {
      selector: '[data-theme="brand"]',
      colorScheme: 'light',
      colors: { primary: '#7c3aed' },
    },
  },
})
```

| Option                        | Default                                                                          | Behavior                                                                                                                                          |
| :---------------------------- | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| `wind3`                       | `false`                                                                          | Enables Wind3 theme mappings and semantic color alpha compatibility. Native numeric spacing and sizes remain unchanged.                           |
| `themeDefaults`               | `true`                                                                           | Emits neutral light/dark colors and color schemes, default shadows, and HTML background/text styles. Set `false` when your CSS defines the theme. |
| `override`                    | omitted                                                                          | Overrides the built-in neutral light and dark colors or adds named themes.                                                                        |
| `override.<name>.colorScheme` | `light` for the built-in light theme; `dark` for dark; omitted for custom themes | Accepts `light` or `dark` only. Emits `color-scheme` on the theme selector; explicit values still apply with `themeDefaults: false`.              |
| `colorStates`                 | `{ hover: 8, active: 12 }`                                                       | Generates missing hover and active colors for configured base colors. Set `false` to disable.                                                     |
| `radius`                      | `0.625rem`                                                                       | Base radius written to `--radius` at `:root`; override per theme when needed.                                                                     |
| `fontSize`                    | `1rem`                                                                           | Base font size written to `--font-size` at `:root`; used by Wind4 typography rules.                                                               |
| `spacing`                     | `0.25rem`                                                                        | Base spacing unit written to `--spacing` at `:root`.                                                                                              |
| `sidebarWidth`                | omitted                                                                          | Writes an explicit global `--sidebar-width` at `:root`; overridable per theme. SidebarFrame supplies its own fallback when unset.                 |

### Theme tokens

For grouped colors such as `primary` and `muted`, strings are shorthand for `{ base: value }`. Expand these colors to override `foreground`, `hover`, or `active` without repeating `base`. Single-value colors such as `control`, `input`, `border`, `ring`, and `backdrop` accept CSS strings only. `control` overrides field fills independently of boundaries and parent surfaces. Available color names and fields match Moraine's semantic colors. Colors and shadows belong inside named `override` entries. `shadows` supports only `surface`, `overlay`, and `input`. The semantic keys emit `--shadow-surface`, `--shadow-overlay`, and `--shadow-input`; see [Backdrop and Semantic Shadows](https://moraine.subf.dev/docs/design.md#backdrop-and-semantic-shadows). Single values use `radius`, `fontSize`, `spacing`, and `sidebarWidth`. Font families are configured in CSS through `--font-sans`, `--font-mono`, and `--font-serif`; see [Typography and Spacing](https://moraine.subf.dev/docs/design.md#typography-and-spacing). Other custom properties belong in your CSS file.

`light` and `dark` default to `:root` and `.dark`. Another name defaults to `[data-theme="name"]`; set `selector` on any entry to use a complete CSS selector. The preset emits the default light colors first, then dark colors, then custom themes in configuration order. Light and dark overrides merge with their respective defaults. A partial custom theme inherits missing variables through the CSS cascade, including variables used by portaled overlays when they are declared on an ancestor of the portal.

The built-in light and dark themes emit `color-scheme: light` and `color-scheme: dark`, respectively, so native browser controls follow the theme. `colorScheme` has type `'light' | 'dark'` and is configured inside `override.<name>`, rather than at the top level. Its declaration follows the entry's `selector`, including custom selectors for light and dark. Custom themes inherit the CSS color scheme unless explicitly configured. `themeDefaults: false` omits the default color schemes while retaining explicit overrides.

See [Moraine-specific Variables](https://moraine.subf.dev/docs/design.md#moraine-specific-variables) for the additional form surface, backdrop, shadow, interaction, typography, sidebar, and animation variables. `colorScheme` writes the native CSS `color-scheme` property; it does not create a custom property.

Semantic color alpha modifiers, such as `bg-primary/20`, use `color-mix(in srgb, …)` in supported browsers. With `wind3: true`, Wind3 keeps the unmodified semantic color as its fallback when `color-mix()` is unavailable.

### Wind3 compatibility

Use `presetMoraine({ wind3: true })` with `presetWind3()`. This enables Wind3's semantic token mappings and color alpha compatibility. Numeric spacing and size utilities keep Wind3's native values and explicit theme overrides; changing `--spacing` does not scale them. Use explicit variable utilities such as `p-(--app-padding)` for runtime spacing changes.

With Wind3, configure font sizes through UnoCSS's `theme.fontSize`; `--font-size` does not scale typography. Customize named radiuses through `theme.borderRadius`. With Wind4, local `--font-size` and `--radius` values scale the default typography and radius tokens; customize them through `theme.text` and `theme.radius`. Explicit UnoCSS theme values take precedence in both modes. Use `leading-*` to set line height independently of font size.

### Font families

Configure font families through UnoCSS's theme object, separately from `presetMoraine()` options. Wind4 uses `theme.font`; Wind3 uses `theme.fontFamily`:

```ts title="Wind4 font configuration"
import { defineConfig, presetWind4 } from '@subf/unocss'
import { presetMoraine } from 'moraine/unocss'

export default defineConfig({
  presets: [presetWind4(), presetMoraine()],
  theme: {
    font: {
      sans: 'Inter, system-ui, sans-serif',
      mono: 'ui-monospace, monospace',
    },
  },
})
```

For Wind3, use `presetWind3()` with `presetMoraine({ wind3: true })` and put the same family entries under `theme.fontFamily`. These explicit theme values take precedence over Moraine's variable references. To keep runtime font switching, use values such as `sans: 'var(--font-sans)'` and define the corresponding CSS variables in your stylesheet. Load any custom font files separately.

### Hover and active colors

For grouped semantic colors, when `color-mix(in oklch, …)` is supported, missing states mix the color toward its foreground in the scope where the utility is used. `background` uses the global foreground; other grouped colors use their own foreground with the global foreground as a fallback. An explicit state variable always wins. An explicit state can be a CSS color, a mixing percentage, or a callback:

```ts
presetMoraine({
  colorStates: { hover: 6 }, // active keeps the default 12%
  override: {
    light: {
      colors: {
        primary: {
          base: '#2563eb',
          hover: '#1d4ed8',
          active: ({ selector, color, state, base, foreground, adjustment }) =>
            `color-mix(in oklch, ${base}, ${foreground} ${adjustment ?? 12}%)`,
        },
        secondary: { hover: 5 },
      },
    },
  },
})
```

The callback receives its selector, color name, state, configured base or CSS variable reference, foreground value or reference, and current adjustment. Percentages must be finite values from 0 to 100. `colorStates: false` stops automatic generation; explicit states still emit. Generated utilities retain their CSS fallbacks: active → hover → base.

Without `color-mix(in oklch, …)`, Moraine's state expression falls back to the base color in the utility's scope unless you provide an explicit state color. This keeps nested CSS themes from inheriting a stale state color from their parent. Numeric state overrides need `color-mix()`; use a widely supported CSS color string when a distinct state must work in older browsers. Wind4 itself emits `color-mix(in srgb, …)` for color utilities, so browsers without any `color-mix()` support need Wind3. When CSS supplies a shadcn/ui palette, extend it with Moraine's `--control` variable and disable the built-in palette:

```ts
presetMoraine({ themeDefaults: false })
```

Define your `:root` and `.dark` color variables, including an explicit control surface:

```css
:root {
  --control: rgb(255, 255, 255);
}
.dark {
  --control: rgb(23, 23, 23);
}
```

`themeDefaults: false` omits control values unless explicitly configured in `override`. A shadcn/ui palette alone is no longer sufficient; see the [form surface migration](https://moraine.subf.dev/docs/design.md#form-surface-migration). `control` has no foreground or state family, uses the existing field foreground, and supports explicit utility opacity such as `bg-control/30`. The preset still emits the default `--radius`, `--font-size`, and `--spacing` values when `themeDefaults` is `false`; your CSS can override them. You can add `--primary-hover` and other state variables there when older browsers need a distinct state color. `baseStyles` has been removed; `themeDefaults` is its replacement and also controls the built-in colors.

