HTMX CSS Animations

HTMX adds and removes CSS classes at each stage of the request lifecycle. You write CSS rules that respond to those classes, and the result is smooth animations — fade-ins, slide-ins, highlights, and fade-outs — without writing a single line of animation JavaScript. The animations run entirely in CSS, which means they run on the GPU and stay smooth even on slower devices.

The HTMX Lifecycle CSS Classes

ClassAdded ToWhenRemoved
htmx-requestTrigger elementRequest is in flightResponse received
htmx-swappingTarget elementAbout to swap contentAfter swap delay
htmx-settlingTarget elementAfter swap, during settle periodAfter settle delay
htmx-addedNew content elementJust inserted into the DOMAfter settling

Fade-In New Content

When HTMX adds new content, it temporarily applies htmx-added to the inserted element. Use this class to start the element at opacity 0 and transition to 1:

<style>
  .htmx-added {
    opacity: 0;
    transition: opacity 0.4s ease;
  }
  .htmx-settling {
    opacity: 1;
  }
</style>

<ul id="task-list"></ul>

<form hx-post="/tasks" hx-target="#task-list" hx-swap="beforeend settle:300ms">
  <input type="text" name="title" placeholder="New task">
  <button type="submit">Add</button>
</form>
  Animation timeline:

  New <li> inserted →  htmx-added applied → opacity: 0
    ↓  (next render frame)
  htmx-settling applied → opacity transitions to 1 over 400ms
    ↓
  Classes removed → element stays at opacity: 1

Fade-Out Before Swap

To animate the old content out before the new content appears, use the swap timing modifier to delay the swap and animate with the htmx-swapping class:

<style>
  .htmx-swapping {
    opacity: 0;
    transition: opacity 0.3s ease;
  }
</style>

<div id="panel"
     hx-get="/new-content"
     hx-trigger="click"
     hx-swap="innerHTML swap:300ms">
  Old content here
</div>
  Sequence:

  Click fires request
    ↓
  htmx-swapping added → panel fades to opacity 0 over 300ms
    ↓  (after 300ms)
  New content swapped in → htmx-swapping removed
    ↓
  New content starts at full opacity (or fades in with htmx-added)

Slide-Down New List Items

<style>
  @keyframes slideDown {
    from {
      transform: translateY(-20px);
      opacity: 0;
    }
    to {
      transform: translateY(0);
      opacity: 1;
    }
  }

  .htmx-added li {
    animation: slideDown 0.3s ease forwards;
  }
</style>

<ul id="comments"></ul>

Each new <li> appended by HTMX slides down from 20px above its final position and fades in simultaneously.

Highlight a Row After Update

A yellow flash on a row that was just updated is a common UX pattern. It tells the user which row changed without a full page refresh:

<style>
  @keyframes yellowFlash {
    0%   { background-color: #fff3cd; }
    100% { background-color: transparent; }
  }

  .htmx-settling tr.updated-row {
    animation: yellowFlash 1.5s ease forwards;
  }
</style>

Server returns the updated row with the class added:

<tr id="row-7" class="updated-row">
  <td>Item Seven</td>
  <td>$49.99</td>
</tr>

Skeleton Loading Animation

Show an animated skeleton placeholder while content loads, then replace it with the real content on response:

<style>
  .skeleton-line {
    height: 16px;
    background: linear-gradient(90deg, #eee 25%, #ddd 50%, #eee 75%);
    background-size: 200% 100%;
    animation: shimmer 1.2s infinite;
    border-radius: 4px;
    margin-bottom: 10px;
  }
  @keyframes shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }
</style>

<div hx-get="/article/5" hx-trigger="load" hx-target="this" hx-swap="outerHTML">
  <div class="skeleton-line" style="width:70%"></div>
  <div class="skeleton-line" style="width:100%"></div>
  <div class="skeleton-line" style="width:85%"></div>
</div>

Button State Animation During Request

<style>
  button.htmx-request {
    opacity: 0.6;
    cursor: not-allowed;
    position: relative;
  }
  button.htmx-request::after {
    content: "";
    display: inline-block;
    width: 12px;
    height: 12px;
    margin-left: 8px;
    border: 2px solid #fff;
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
    vertical-align: middle;
  }
  @keyframes spin {
    to { transform: rotate(360deg); }
  }
</style>

<button hx-post="/save" hx-target="#result">Save</button>

While the request is in flight, the button dims and a spinning indicator appears after the button text — entirely through CSS.

Controlling Timing With Swap Modifiers

hx-swap="innerHTML swap:300ms settle:400ms"
  • swap:300ms — Wait 300ms before inserting new content (gives the fade-out animation time to complete).
  • settle:400ms — Keep the htmx-settling class for 400ms after insertion (gives the fade-in animation time to complete).

Coordinate these values with your CSS transition duration for smooth, glitch-free animations.

Key Takeaway

HTMX CSS animations work by targeting the lifecycle classes HTMX applies during requests: htmx-request, htmx-swapping, htmx-settling, and htmx-added. Write CSS transitions or keyframe animations triggered by these classes. Use swap and settle timing modifiers in hx-swap to coordinate animation duration with content replacement timing. The result is smooth, GPU-accelerated page transitions with no animation JavaScript required.

Leave a Comment

Your email address will not be published. Required fields are marked *