Tailwind CSS with htmx: Styling Hypermedia Applications
AI generated
</>
tw
Tailwind CSS · htmx · Hypermedia · Server-Side Rendering
Tailwind CSS with htmx
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.

17 min read htmx 2.x · Tailwind CSS v4 · Hypermedia-driven Applications Server-Side Rendering · Progressive Enhancement

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.

11. FAQ: Tailwind CSS with htmx

1Does htmx with Tailwind need a Node build?
No, the standalone Tailwind CLI runs independent of Node.js and scans server templates directly.
2How do I style a loading indicator?
Through the htmx-indicator class, whose visibility htmx controls. The look comes entirely from Tailwind classes like animate-spin.
3Animate transitions between swaps?
Transition class in the fragment plus an event listener for htmx:afterSwap, or the native View Transitions API since htmx 2.
4Style an out-of-band swap consistently?
hx-swap-oob updates elements outside the target. Both should draw classes from the same template partial.
5Why are Tailwind classes missing from fragments?
Usually the fragment template sits outside the scan path, or the class is assembled from data at runtime and needs a safelist.
6Forms without duplicate validation?
The server validates once and returns errors directly as a styled HTML fragment, no second logic in the client.
7Suitable for admin backends?
Very well, CRUD interfaces benefit from fast time to interactive without a complete SPA framework.
8Combine hx-boost with Tailwind?
Yes, hx-boost intercepts link clicks and loads via AJAX, without changes to markup or Tailwind classes.
9Difference from an islands architecture?
Islands hydrate components client-side, htmx relies entirely on server fragments with a minimal JavaScript footprint.
10Dark mode with htmx fragments?
Yes, dark: classes work like any other utility class, the dark mode state sits on the html element, independent of htmx.