Optional Tailwind CSS Integration
Tailwind CSS is optional. The built-in Litsx styling path remains Lit's css tagged template and Component.styles. Choose @litsx/tailwind when a project wants Tailwind CSS v4 utilities inside component-owned Shadow DOM or Light DOM output.
The root package is bundler-neutral. Its supported Vite adapter uses the official @tailwindcss/vite plugin rather than Tailwind's private compiler APIs.
Install
npm install -D @litsx/tailwind @litsx/vite-plugin \
@tailwindcss/vite tailwindcss viteThe Vite integration supports Tailwind CSS 4.3+, Vite 7.3 or 8, and Litsx 1.x.
Configure Vite
// vite.config.js
import { defineConfig } from "vite";
import { litsxTailwind } from "@litsx/tailwind/vite";
export default defineConfig({
plugins: litsxTailwind({
integration: {
entry: "./src/tailwind.css",
},
}),
});Pass the complete array returned by litsxTailwind(...) directly to plugins. The adapter keeps Litsx compilation, Tailwind materialization, virtual CSS, and cleanup in the required order.
Create the CSS entry referenced by integration.entry:
/* src/tailwind.css */
@import "tailwindcss" source(none);
@theme {
--color-brand: oklch(62% 0.18 255);
}source(none) is recommended because Litsx owns candidate routing. The entry still owns theme values, preflight, plugins, and authored global CSS.
Component-owned utilities
Literal classes and finite values referenced from class or className belong only to the component that uses them:
const SIZE_CLASSES = {
sm: "h-8 px-3",
lg: "h-12 px-6",
} as const;
export function ActionButton({ size = "sm" }) {
return <button class={SIZE_CLASSES[size]}>Save</button>;
}Constants, maps, finite branches, and exact imported values are resolved statically. A Shadow DOM component receives only its exact utilities as a component-owned CSSResult; a sibling component in the same module does not receive them.
JSX outside a Litsx component class belongs to the document. This includes Storybook render functions and other free Light DOM templates. In a mixed module, their utilities enter the global sheet while component-only utilities remain attached to their owning Shadow or Light DOM component. A utility used by both destinations is generated in both so each output remains independently usable.
Pure Lit class bodies are opaque to the integration: their templates and static styles remain owned by Lit and are not treated as free document JSX.
Component.styles remains available as an explicit local guard for finite utilities that cannot be reached from markup. The integration consumes strings, arrays, objects, and imported constants at build time without passing them to Lit as invalid runtime styles:
DynamicPanel.styles = [
baseStyles,
{ red: "bg-red-600", green: "bg-green-600" },
];Dynamic class names
A runtime-generated class needs a finite integration safelist:
export function ColorSwatch({ color }) {
return <span class={`bg-${color}-600`} />;
}litsxTailwind({
integration: {
entry: "./src/tailwind.css",
safelist: ["bg-red-600", "bg-green-600"],
},
});Only safelist entries matching that component's bg-*-600 pattern enter its stylesheet. Unrelated safelist utilities are not copied into the shadow root. Fully opaque bindings should enumerate their allowed utilities through Component.styles.
Shadow DOM and Light DOM
Shadow components receive the shared preflight, their exact component utility sheet, and inherited or authored Component.styles in normal Lit order. Theme and preflight are emitted once at document level.
Tailwind utilities such as shadow-*, ring-*, and translate-* rely on global @property declarations. The integration emits those through an inert infrastructure sheet so lazy components work without exposing their utility selectors globally.
Light DOM follows the ordinary compiler policy:
globalemits document-level utilities.scopedemits utilities inside@scope (...) to (...)and stops them at nested Litsx roots.nonedisables automatic generated Light DOM styles.
Scoped Light DOM requires native CSS @scope support: Chrome and Edge 118+, Safari and iOS 17.4+, or Firefox 146+. Use global when older browsers, including Firefox ESR 140, are in scope. React compatibility selects global to preserve its document-level styling model.
Sources and lazy components
litsxTailwind({
litsx: {},
tailwind: {},
integration: {
entry: "./src/tailwind.css",
sources: ["./src/**/*.{html,js,jsx,ts,tsx}"],
safelist: [],
},
});litsxforwards options to@litsx/vite-plugin.tailwindforwards options to the official@tailwindcss/viteplugin.sourcescontributes only shared infrastructure required before lazy modules load; it is not a fallback global utility scanner and does not leak component utility selectors globally.safelistprovides finite candidates for non-finite component patterns.
The same routing covers development updates, production builds, SSR, hydration, and lazily imported components.
Parallel and multi-entry builds
A single project context safely supports parallel component transforms. Each component and Vite entry retains only its own utility candidates, while client and SSR transforms of the same module reuse stable style metadata. Concurrently generated sheets therefore do not leak sibling classes between shadow roots or entry chunks.
The Vite adapter also preserves query suffixes on its virtual CSS modules, including ?inline requests made during real builds. Create one context per project, rather than one per module, and reuse it across development, production, client, and SSR transforms.
Custom build integrations
Framework adapters can compose createTailwindContext(...), createTailwindAuthoringPlugin(...), createTailwindOutputPlugin(...), and withTailwindCompiler(...) from the bundler-neutral @litsx/tailwind entry. Create one context per project and reuse it across client, SSR, and watch transforms.
Vite-specific advanced integrations can use withTailwindViteCompiler(...), createTailwindVirtualPlugin(...), and createTailwindPropertyCleanupPlugin(...) when another framework owns plugin ordering. Ordinary Vite applications should prefer litsxTailwind(...).