@davaux/scoped-css
Location-based CSS scoping for Davaux. CSS files imported from src/islands/ are automatically scoped — every class name gets a short deterministic hash suffix so island styles never leak into the rest of the page. No .module.css convention needed.
Installation
npm install @davaux/scoped-css
Setup
Register the plugin in davaux.config.ts:
// davaux.config.ts
import { defineConfig } from 'davaux/config'
import { scopedCss } from '@davaux/scoped-css'
export default defineConfig({
plugins: [scopedCss()],
})
Add the ambient type declaration so TypeScript accepts CSS default imports:
// src/env.d.ts
/// <reference types="@davaux/scoped-css/env" />
How scoping works
CSS imported from inside src/islands/ (or CSS files that live there) are intercepted at build time. Each class name is rewritten with a stable 5-character hash derived from the file path:
/* src/islands/Button.css — authored */
.button { background: blue; }
.active { background: darkblue; }
/* emitted to the bundle */
.button-a1b2c { background: blue; }
.active-a1b2c { background: darkblue; }
The import returns a Record<string, string> mapping original names to their scoped counterparts:
import styles from './Button.css'
// styles.button === 'button-a1b2c'
// styles.active === 'active-a1b2c'
CSS outside src/islands/ is never scoped — global styles and layout CSS work exactly as before.
Basic usage
// src/islands/Button.tsx
import styles from './Button.css'
import { island } from 'davaux'
export const Button = island('Button', () => {
return <button class={styles.button}>Click me</button>
})
Opting out with ?global
To import a CSS file without scoping — a reset sheet, a third-party stylesheet — append ?global:
import './normalize.css?global'
The file is passed through the normal CSS pipeline without any class name rewriting.
cx() — class name composition
The cx helper from @davaux/scoped-css/client composes class names from strings and condition maps:
import { cx } from '@davaux/scoped-css/client'
cx(styles.button, styles.large)
// → 'button-a1b2c large-a1b2c'
cx(styles.button, { [styles.active]: isActive })
// → 'button-a1b2c active-a1b2c' (when isActive is true)
// → 'button-a1b2c' (when isActive is false)
Falsy arguments (false, null, undefined) are skipped:
cx(styles.button, isLoading && styles.loading)
// → 'button-a1b2c loading-a1b2c' or 'button-a1b2c'
Reactive cx() with signals
When any condition in a map is a signal getter, cx returns a reactive getter () => string instead of a plain string. The JSX runtime wraps it in a createEffect so only the class attribute updates when the signal changes — the rest of the DOM is untouched.
import { createSignal } from 'davaux'
import { cx } from '@davaux/scoped-css/client'
import styles from './Button.css'
const [active, setActive] = createSignal(false)
// cx detects that `active` is a function → returns () => string
const className = cx(styles.button, { [styles.active]: active })
<button class={className} onClick={() => setActive((v) => !v)}>
Toggle
</button>
Or inline directly in JSX — the JSX runtime handles it automatically:
<button class={cx(styles.button, { [styles.active]: active })}>
Toggle
</button>
cx argument types
| Argument | Effect |
|---|---|
string | Included as-is |
false | null | undefined | Skipped |
{ [cls]: boolean } | Includes cls when value is true |
{ [cls]: () => boolean } | Reactive — cx returns () => string |
TypeScript
The @davaux/scoped-css/env reference adds a declare module '*.css' ambient declaration so TypeScript treats CSS default imports as Record<string, string>. Add it to src/env.d.ts once per project:
/// <reference types="@davaux/scoped-css/env" />
If you need the cx argument type in your own helpers:
import type { CxArg } from '@davaux/scoped-css/client'
function buildClass(...args: CxArg[]): string | (() => string) {
// ...
}
SCSS and SASS
Because scoping happens on the output CSS (after esbuild processes the file), SCSS/SASS works without any extra configuration — just run the SCSS through a pre-processor first and import the resulting .css file, or use an esbuild SCSS plugin alongside @davaux/scoped-css. The hash is derived from the source file path so scoped names stay stable regardless of the CSS toolchain upstream.
Notes
- Class names are hashed from the absolute file path — the same source file always produces the same scoped names across builds.
- Only standalone class selectors are rewritten. Attribute selectors (
[class~="btn"]), string values, and CSS custom properties are left untouched. - The scoped CSS and JS shim are both emitted to the existing bundle — no separate CSS file per component.