Tailwind
@voltage/tailwind compiles Tailwind CSS v4 at application startup using Tailwind’s native Node API and injects the result into rendered HTML automatically. HTMX partial updates are detected and skipped — the <style> tag carries hx-preserve="true" so HTMX never replaces it.
Installation
Section titled “Installation”yarn add @voltage/tailwind @voltage/async-context @voltage/event-manager @voltage/jsxBasic usage
Section titled “Basic usage”Call forRoot() once in your root module with a path to your theme CSS file:
import { TailwindModule } from '@voltage/tailwind';
@Module({ imports: [ TailwindModule.forRoot({ base: '/app/assets/theme.css', }), ],})export class AppModule {}Tailwind compiles on ApplicationBootstrapEvent and injects the result into the <head> of every full-page response. No additional wiring is needed.
Theme file
Section titled “Theme file”The base path points to a standard Tailwind v4 CSS entry file. A minimal setup imports Tailwind, configures content scanning, and sets design tokens:
@import 'tailwindcss';@plugin "@voltage/htmx/tailwind";
@source "/app/dist/**/*.js";
@theme { --color-primary: #1581e4; --color-danger: #ec1f00;}@source tells Tailwind where to scan for class names. Point it at your compiled JS output — the scanner extracts class names from the rendered JSX strings.
If you are using @voltage/ui, import its theme to get the full design token set:
@import 'tailwindcss';@import '@voltage/ui/theme';@plugin "@voltage/htmx/tailwind";
@source "/app/dist/**/*.js";
@theme { /* override tokens as needed */ --color-primary: #1581e4;}HTMX variants
Section titled “HTMX variants”Importing @plugin "@voltage/htmx/tailwind" adds four variants that map to HTMX’s lifecycle classes:
| Variant | Active when |
|---|---|
htmx-request: | Request is in flight (htmx-request class) |
htmx-settling: | Response has swapped, settling in progress |
htmx-swapping: | Swap is in progress |
htmx-added: | Element was just added to the DOM |
A common use is dimming a button or showing a spinner while a request is in flight:
<button hx-post={this.url.generate(CartController, 'add')} hx-swap="none" class="htmx-request:opacity-50 htmx-request:cursor-wait"> Add to cart</button>Or on an hx-indicator element:
<form {...action(this.url.generate(AuthController, 'login'))} hx-indicator="#spinner"> {/* fields */} <button type="submit"> Log in <span id="spinner" class="opacity-0 htmx-request:opacity-100">…</span> </button></form>Both variants match on the element itself and on any descendant — htmx-request:opacity-50 works whether the class is on the element or on a parent.
Collecting CSS from modules
Section titled “Collecting CSS from modules”TailwindCollectEvent fires before each compilation pass. Listen to it in any module to contribute CSS fragments — this is how @voltage/ui injects its component styles without requiring manual imports in the theme file:
import { OnEvent } from '@voltage/event-manager';import { TailwindCollectEvent } from '@voltage/tailwind';
@Injectable()export class BadgeStyles { @OnEvent(TailwindCollectEvent) onCollect(event: TailwindCollectEvent) { event.addFragment(` @layer components { .badge { @apply inline-flex items-center rounded px-2 py-0.5 text-sm; } } `); }}addFile(path) is available when the CSS lives in a separate file:
@OnEvent(TailwindCollectEvent)onCollect(event: TailwindCollectEvent) { event.addFile(join(__dirname, 'badge.css'));}Fragments are concatenated with the base theme file before compilation, so all Tailwind directives (@apply, @layer, @theme) work as expected.
Hot reloading
Section titled “Hot reloading”Set watch: true to recompile whenever the theme file or any of its imports change. The recompiled CSS is served on the next request — no restart required.
TailwindModule.forRoot({ base: '/app/assets/theme.css', watch: process.env.NODE_ENV !== 'production',})Configuration
Section titled “Configuration”interface TailwindConfiguration { /** Absolute path to the Tailwind CSS entry file. */ base: string; /** Watch the theme file for changes and recompile automatically. Defaults to false. */ watch?: boolean;}