Summary
nude-element loads shadow DOM styles at runtime via fetch(). Each element class declares a static property (e.g., static styles = "./my-slider.css") and at runtime the library resolves the full URL, fetches the CSS text, creates a CSSStyleSheet, and adopts it onto the shadow root.
This breaks with all bundlers in production (CSS files are never emitted, so fetch() 404s), and additionally in Vite's dev server (which serves .css as JS modules).
The issue affects all five static style properties that go through the defineStyles() → fetch() path: styles, shadowStyles, globalStyles, lightStyles, and documentStyles.
| Bundler |
Dev server |
Production build |
| Vite |
❌ Wrong Content-Type |
❌ CSS not emitted |
| webpack |
✅ Works |
❌ CSS not emitted |
| Rollup / esbuild |
✅ Works (no dev server) |
❌ CSS not emitted |
| Parcel |
✅ Likely works |
❌ CSS not emitted |
Root Cause
static styles = "./my-slider.css" is an opaque string literal — not an ES import. No bundler tracks it as a dependency, so the .css files are never emitted to the output directory.
The fetch() path in code
- Class setup —
first_constructor_static hook in src/plugins/styles/base.js calls this.defineStyles(this.styles).
defineStyles() — if the value is a string, wraps it as { url: string } and resolves the full URL.
connected hook — calls adoptStyle(options, roots) from src/plugins/styles/util/adopt-style.js.
adoptStyle() — three branches:
ref instanceof CSSStyleSheet → use directly
ref.css → new CSSStyleSheet() + replaceSync(css) ✅
- URL fallback →
cachedFetch(fullUrl) → fetch() ❌ breaks with bundlers
The { css: string } branch bypasses fetch() entirely, which is why inlining CSS at build time works.
Possible Solutions
-
import with CSS module attributes — Use import "./foo.css" with {type: "css"} which bundlers can track as a dependency. Supported almost everywhere except Safari, but could be used with a fetch() fallback for unsupported browsers. A helper function could detect support and use either method accordingly.
-
Document the { css } pattern as the recommended approach for bundled projects (already works, zero library changes needed).
-
Ship an official Vite plugin as a package (e.g., nude-element/vite). A proof-of-concept plugin that rewrites style string literals into ?inline CSS imports (which Vite resolves as plain CSS strings):
function inlineShadowStyles() {
const RE = /\bstatic\s+(styles|shadowStyles|globalStyles|lightStyles|documentStyles)\s*=\s*(['"])(\.\/[^'"]+\.css)\2/g;
return {
name: "inline-shadow-styles",
transform(code, id) {
if (!id.endsWith(".js")) return null;
RE.lastIndex = 0;
if (!RE.test(code)) return null;
RE.lastIndex = 0;
const imports = [];
let i = 0;
const newCode = code.replace(RE, (_match, prop, _quote, cssPath) => {
const varName = `__shadow_styles_${i++}`;
imports.push(`import ${varName} from "${cssPath}?inline";`);
return `static ${prop} = [{ css: ${varName} }]`;
});
return { code: imports.join("\n") + "\n" + newCode, map: null };
},
};
}
This handles all five static style properties and works for both dev and production builds.
Summary
nude-elementloads shadow DOM styles at runtime viafetch(). Each element class declares a static property (e.g.,static styles = "./my-slider.css") and at runtime the library resolves the full URL, fetches the CSS text, creates aCSSStyleSheet, and adopts it onto the shadow root.This breaks with all bundlers in production (CSS files are never emitted, so
fetch()404s), and additionally in Vite's dev server (which serves.cssas JS modules).The issue affects all five static style properties that go through the
defineStyles()→fetch()path:styles,shadowStyles,globalStyles,lightStyles, anddocumentStyles.Root Cause
static styles = "./my-slider.css"is an opaque string literal — not an ES import. No bundler tracks it as a dependency, so the.cssfiles are never emitted to the output directory.The
fetch()path in codefirst_constructor_statichook insrc/plugins/styles/base.jscallsthis.defineStyles(this.styles).defineStyles()— if the value is a string, wraps it as{ url: string }and resolves the full URL.connectedhook — callsadoptStyle(options, roots)fromsrc/plugins/styles/util/adopt-style.js.adoptStyle()— three branches:ref instanceof CSSStyleSheet→ use directlyref.css→new CSSStyleSheet()+replaceSync(css)✅cachedFetch(fullUrl)→fetch()❌ breaks with bundlersThe
{ css: string }branch bypassesfetch()entirely, which is why inlining CSS at build time works.Possible Solutions
importwith CSS module attributes — Useimport "./foo.css" with {type: "css"}which bundlers can track as a dependency. Supported almost everywhere except Safari, but could be used with afetch()fallback for unsupported browsers. A helper function could detect support and use either method accordingly.Document the
{ css }pattern as the recommended approach for bundled projects (already works, zero library changes needed).Ship an official Vite plugin as a package (e.g.,
nude-element/vite). A proof-of-concept plugin that rewrites style string literals into?inlineCSS imports (which Vite resolves as plain CSS strings):This handles all five static style properties and works for both dev and production builds.