Styling hypermedia applications without an SPA framework
htmx brings AJAX, swap transitions and server-side rendering back into simple HTML attributes, and Tailwind CSS supplies the matching utility-first styling without a separate frontend build. This article shows how loading states, transitions between swaps and dynamically loaded fragments get styled consistently, without needing a complete JavaScript framework.
Table of Contents
- 1. Why htmx and Tailwind CSS fit together
- 2. Setup: adding htmx and Tailwind CSS without a build framework
- 3. Loading states: htmx-indicator and Tailwind animations
- 4. Accompanying swap strategies visually: transitions between fragments
- 5. Combining the View Transitions API with htmx and Tailwind
- 6. Styling error states and validation from server responses
- 7. Out-of-band swaps: updating multiple UI regions consistently
- 8. Content scanning: capturing server templates correctly
- 9. htmx plus Tailwind compared to SPA approaches
- 10. Summary
- 11. FAQ
1. Why htmx and Tailwind CSS fit together
htmx extends HTML with attributes like hx-get, hx-post and hx-swap, letting any element trigger AJAX requests and swap parts of the page, without a single line of custom JavaScript. The server returns finished HTML instead of JSON, which htmx inserts directly into the page. Tailwind CSS fits in seamlessly on a technical level, because utility classes work regardless of whether a fragment arrives in the DOM on the first page load or through a later htmx request.
The decisive difference from classic SPA frameworks: with htmx, all rendering logic stays on the server, and Tailwind CSS never has to consider client-side hydration or component structure. Every HTML fragment rendered by the server already carries its Tailwind classes fully built in, exactly as with the initial page load. This significantly reduces complexity, because no state has to be synchronized between client and server, only the display of HTML with Tailwind classes.
For teams switching from complex React or Vue setups to a server-centric approach, the combination of htmx and Tailwind CSS is particularly attractive, because backend developers without deep JavaScript knowledge can build complete, interactive interfaces. Templates in Django, Rails, Symfony or Laravel remain the single source of truth for markup and styling at the same time.
2. Setup: adding htmx and Tailwind CSS without a build framework
The typical setup requires no Node-based frontend build system in the strict sense. htmx is included as a single JavaScript file, and Tailwind CSS v4 can be run directly against the server templates through the standalone CLI (@tailwindcss/cli), independent of the backend language. This produces a compiled CSS file that the server delivers like any other static resource.
It matters that the Tailwind CLI runs in watch mode alongside the backend server during development, so new classes in server templates land in the compiled CSS immediately. In production environments a single build step in the deployment pipeline is enough, run before the application server starts.
# Install the standalone Tailwind CSS CLI (no Node.js project required)
curl -sLo tailwindcss https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
chmod +x tailwindcss
# Watch server-rendered templates for class names during development
./tailwindcss -i ./src/input.css -o ./public/app.css --watch
# One-off minified build for production deployment
./tailwindcss -i ./src/input.css -o ./public/app.css --minify
3. Loading states: htmx-indicator and Tailwind animations
htmx ships with the htmx-indicator class, a built-in mechanism for loading indicators that makes an element visible during a running request and hides it again afterward. Tailwind CSS controls the appearance of this indicator entirely through utility classes like opacity-0, transition-opacity and a spinner animation through animate-spin. The advantage: htmx only handles the timing of becoming visible, while Tailwind CSS determines the complete visual appearance.
For buttons that should appear disabled during a running request, hx-indicator gets combined with a Tailwind class set that adjusts cursor, opacity and pointer-events at the same time. This pattern prevents duplicate form submissions, without needing custom JavaScript logic for debouncing, because htmx itself marks the request as in progress.
<!-- Loading indicator styled entirely with Tailwind classes -->
<button
hx-post="/api/orders"
hx-target="#order-summary"
hx-indicator="#order-spinner"
class="relative inline-flex items-center gap-2 rounded-lg bg-sky-600 px-4 py-2 text-sm font-semibold text-white hover:bg-sky-700 disabled:opacity-50">
<span>Submit order</span>
<svg id="order-spinner" class="htmx-indicator w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
</button>
<!--
htmx toggles the htmx-indicator class's opacity from 0 to 100 during the request.
Tailwind controls the transition timing via the .htmx-indicator base rule.
-->
4. Accompanying swap strategies visually: transitions between fragments
htmx offers several strategies through hx-swap for how a new fragment replaces an existing element, such as innerHTML, outerHTML or beforeend. Tailwind CSS can accompany these changes visually by applying CSS transitions to the inserted fragment. The common pattern: the new fragment starts with opacity-0 and a slight offset, and a short CSS transition fades it in visibly once it has landed in the DOM.
For these transitions to work reliably, the transition class must already be present in the HTML fragment delivered by the server, not added afterward through JavaScript. A small htmx event listener pattern that sets an additional Tailwind class for the entering state after htmx:afterSwap handles the transition from a defined start state to a defined end state, without any additional library.
<!-- Fragment enters with a Tailwind transition after htmx swaps it in -->
<div id="cart-items" hx-swap-oob="true"
class="opacity-0 translate-y-2 transition-all duration-300 ease-out">
<!-- Server-rendered cart line items go here -->
</div>
<script>
// Trigger the enter transition once htmx has inserted the fragment
document.body.addEventListener('htmx:afterSwap', (event) => {
const target = event.detail.target;
requestAnimationFrame(() => {
target.classList.remove('opacity-0', 'translate-y-2');
});
});
</script>
5. Combining the View Transitions API with htmx and Tailwind
Since htmx 2, there is built-in support for the browser's View Transitions API through hx-swap="... transition:true". This lets the browser itself handle a smooth transition between the old and new DOM state, without any manual CSS transition logic. Tailwind CSS only controls the static appearance of the old and new state here, while the browser computes the actual animation.
For finer styling of the transition animation itself, the CSS pseudo-elements ::view-transition-old and ::view-transition-new come into play, which sit outside the regular Tailwind utility system and are therefore added in a small @layer utilities block. This combination of a native browser API and targeted CSS next to Tailwind is significantly lighter than any JavaScript animation library and works in htmx applications without an additional dependency.
6. Styling error states and validation from server responses
For forms submitted through hx-post, the server returns an HTML fragment with the same form on a validation error, extended with error messages and adjusted Tailwind classes for the affected fields. The server decides server-side which classes get set for an invalid field, such as border-red-400 instead of border-slate-300, and returns the complete, already styled fragment.
This pattern differs fundamentally from client-side form validation: there is no duplicate validation logic in JavaScript and on the server, because the display of the error state comes directly out of the server-side validation. For immediate feedback without a full form submission, hx-trigger="blur" on individual fields gets combined with a small server-side validation endpoint that returns only the affected field including Tailwind error classes.
7. Out-of-band swaps: updating multiple UI regions consistently
A common use case: clicking "Add to cart" should update both the product list and the cart badge in the navigation at the same time, even though both regions sit in different places in the DOM. htmx solves this through hx-swap-oob="true", letting a server fragment update an element somewhere else on the page, independent of the request's actual hx-target.
For Tailwind CSS this means both affected elements, the main target and the out-of-band element, need to use consistent class sets, so a change such as the badge count looks visually the same as on the initial page load. A recurring class fragment, such as for the badge, should therefore come server-side from a single template partial that is used both on first load and on every out-of-band update, so Tailwind classes don't diverge into two versions.
<!-- Server response updates two unrelated DOM regions in one request -->
<div id="product-list-item-42">
<!-- Main hx-target update: product card marked as added -->
<span class="text-xs font-semibold text-emerald-700">Added to cart</span>
</div>
<span id="cart-badge" hx-swap-oob="true"
class="inline-flex items-center justify-center rounded-full bg-sky-600 text-white text-xs font-bold w-5 h-5">
3
</span>
8. Content scanning: capturing server templates correctly
Tailwind CSS v4 scans all source files sitting in the project directory at build time, regardless of whether they are Jinja, Twig, ERB or plain HTML templates. For htmx applications this means: even fragment templates that are only rendered for htmx requests and never represent a full page must sit in the scan path of the Tailwind configuration. If fragments are placed in a separate directory outside the default scan range, their classes don't show up in the final CSS bundle.
A second pitfall concerns classes assembled server-side from database values, such as status colors coming from a configuration table. These class names don't exist as text in the source code at build time and therefore need to be added through a safelist in the CSS file, so they are actually present in the compiled stylesheet once the server outputs them at runtime.
9. htmx plus Tailwind compared to SPA approaches
The decision between htmx with server-side rendering, a classic single-page application and hybrid approaches like islands architectures depends heavily on team size, interaction complexity and the desired time to interactive.
| Approach | Client-side JavaScript | Time to interactive | Tailwind integration |
|---|---|---|---|
| htmx plus Tailwind CSS | Minimal, one script tag | Very fast | CLI scans server templates directly |
| Classic SPA (React/Vue) | High, framework plus state | Slower until hydration finishes | Build pipeline required |
| Islands architecture | Per island only | Good, selective hydration | Multiple build targets |
| Pure server rendering without htmx | None | Very fast | Full page reloads on interaction |
htmx plus Tailwind CSS sits in practice between pure server rendering and a full SPA, with the smallest JavaScript footprint while still offering modern, partial page updates. For projects with manageable interaction complexity, such as classic CRUD interfaces or admin backends, this combination delivers the fastest time to interactive at the lowest maintenance cost.
Mironsoft
Hypermedia applications, server-side rendering and Tailwind setups
Ready to build interactive interfaces without an SPA framework?
We build htmx applications with cleanly styled server rendering, consistent loading states and Tailwind CSS, without requiring your team to learn a complete JavaScript framework.
htmx architecture
Planning server templates, fragment structure and swap strategies
Tailwind setup
Standalone CLI integration into existing backend pipelines
Migration
Gradually moving existing SPA sections to a hypermedia approach
10. Summary
Tailwind CSS and htmx complement each other because both rest on the same underlying principle: introduce as little additional complexity as possible to solve a concrete problem. htmx delivers interactivity through HTML attributes and server-rendered fragments, Tailwind CSS delivers styling through utility classes directly in exactly those fragments. Loading states through htmx-indicator, transitions through Tailwind transitions or the View Transitions API, and out-of-band swaps for multiple UI regions can all be implemented without an additional JavaScript framework.
The most important point for production use is content scanning: even pure fragment templates that never represent a complete page must sit in the scan path of the Tailwind configuration, otherwise their classes are missing from the compiled CSS. For teams working server-centrically and wanting to keep the JavaScript footprint minimal, htmx with Tailwind CSS is one of the fastest paths to a modern, interactive interface.
Tailwind CSS with htmx — Key Takeaways
Setup
Run the standalone Tailwind CLI against server templates, independent of the backend language, no Node project strictly required.
Loading states
htmx-indicator controls visibility, Tailwind classes like animate-spin control the complete appearance.
Swaps and transitions
Enter transitions through Tailwind classes after htmx:afterSwap, or the native View Transitions API since htmx 2.
Content scanning
Fragment templates must sit in the scan path, classes dynamically assembled from data need a safelist.