Skip to content

Introduction to Alpine.js and HTMX

Adding interactivity to your web pages doesn’t have to involve complex build systems or large frameworks.

Alpine.js and HTMX provide lightweight, easy-to-use solutions that work directly in the browser using a CDN. Alpine.js focuses on front-end interactivity by allowing you to add declarative behavior and manage state directly in HTML, making it ideal for dynamic UI components. HTMX, on the other hand, is centered on server-driven interactions, letting you make AJAX requests and update HTML content dynamically without writing JavaScript. Together, they provide flexible tools for different kinds of interactivity while avoiding the overhead of larger frameworks or build systems. This makes them perfect for projects that don’t require modern JavaScript build tools.

Why Alpine.js and HTMX?

No Build System Required:

Both libraries work directly in the browser via CDN. No npm, Webpack, Vite, or complex tooling needed.

Progressive Enhancement:

Add interactivity to existing HTML without rewriting everything. They enhance traditional server-rendered applications.

Small Bundle Size:

  • Alpine.js: ~15KB gzipped
  • HTMX: ~14KB gzipped
  • Compare to React (~40KB) + ReactDOM (~130KB)

Simple Learning Curve:

If you know HTML, you can start immediately. No need to learn JSX, virtual DOM, or component lifecycle.

Different Philosophies:

  • Alpine.js: “jQuery for the modern web” - frontend reactivity
  • HTMX: “Access modern browser features via HTML” - server-driven

When to Use What

Use Alpine.js when:

  • You need reactive UI components (dropdowns, modals, tabs)
  • State management is mostly client-side
  • You want Vue-like syntax without the framework
  • Building SPAs with minimal JavaScript

Use HTMX when:

  • Your backend renders HTML (Django, Laravel, Rails, PHP)
  • You want server-driven interactions
  • You prefer server-side logic over client-side
  • Building traditional multi-page applications

Use both when:

  • You want Alpine for UI interactions + HTMX for server updates
  • Building hybrid applications
  • You need best of both worlds

Use React/Vue/Svelte when:

  • Building complex single-page applications
  • Need large ecosystem and community
  • Team is already skilled in these frameworks
  • Require advanced state management

Alpine.js

Alpine.js brings reactive and declarative behavior to your HTML. Think of it as a lightweight Vue.js that doesn’t require a build step.

Installing Alpine.js

Alpine.js can be added directly to your project using a CDN. There’s no need for npm, Webpack, or other build tools—just include the script tag in your HTML.

Include Alpine.js:

Add this script tag to your HTML file inside the <head> or just before the closing <body> tag:

<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>

The defer attribute ensures that Alpine.js runs after your HTML has fully loaded.

Version Pinning

For production, pin to a specific version:

<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.5/dist/cdn.min.js" defer></script>

Core Alpine.js Directives

x-data:

Declares a new component scope with reactive data.

<div x-data="{ open: false }">
  <!-- Component content -->
</div>

x-show / x-if:

Conditionally show/hide elements.

<div x-data="{ show: true }">
  <button @click="show = !show">Toggle</button>
  <p x-show="show">This text can be toggled</p>
</div>

x-text / x-html:

Set text or HTML content.

<div x-data="{ message: 'Hello, Alpine!' }">
  <p x-text="message"></p>
</div>

x-on (@):

Listen to events.

<button x-on:click="count++">Click me</button>
<!-- Shorthand -->
<button @click="count++">Click me</button>

x-model:

Two-way data binding for form inputs.

<div x-data="{ message: '' }">
  <input type="text" x-model="message">
  <p x-text="message"></p>
</div>

x-bind (:):

Bind attributes to data.

<div x-data="{ color: 'red' }">
  <p :style="'color: ' + color">Colored text</p>
</div>

Basic Alpine.js Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Alpine.js Example</title>
  <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
</head>
<body>
  <div x-data="{ count: 0 }">
    <button x-on:click="count++">Increase</button>
    <button @click="count--">Decrease</button>
    <button @click="count = 0">Reset</button>
    <p x-text="'You clicked ' + count + ' times.'"></p>
  </div>
</body>
</html>

Practical Alpine.js Examples

Dropdown menu:

<div x-data="{ open: false }" @click.away="open = false">
  <button @click="open = !open">Menu</button>
  <div x-show="open" class="dropdown-menu">
    <a href="#">Item 1</a>
    <a href="#">Item 2</a>
    <a href="#">Item 3</a>
  </div>
</div>

Tabs:

<div x-data="{ activeTab: 'home' }">
  <div class="tabs">
    <button @click="activeTab = 'home'" :class="{ 'active': activeTab === 'home' }">Home</button>
    <button @click="activeTab = 'profile'" :class="{ 'active': activeTab === 'profile' }">Profile</button>
    <button @click="activeTab = 'settings'" :class="{ 'active': activeTab === 'settings' }">Settings</button>
  </div>

  <div class="tab-content">
    <div x-show="activeTab === 'home'">Home content</div>
    <div x-show="activeTab === 'profile'">Profile content</div>
    <div x-show="activeTab === 'settings'">Settings content</div>
  </div>
</div>

Modal:

<div x-data="{ open: false }">
  <button @click="open = true">Open Modal</button>

  <div x-show="open" @click="open = false" class="modal-overlay">
    <div @click.stop class="modal-content">
      <h2>Modal Title</h2>
      <p>Modal content goes here</p>
      <button @click="open = false">Close</button>
    </div>
  </div>
</div>

Form with validation:

<div x-data="{ 
  email: '', 
  password: '',
  errors: {}
}">
  <form @submit.prevent="
    errors = {};
    if (!email.includes('@')) errors.email = 'Invalid email';
    if (password.length < 6) errors.password = 'Password too short';
    if (Object.keys(errors).length === 0) alert('Form valid!');
  ">
    <input type="email" x-model="email" placeholder="Email">
    <p x-show="errors.email" x-text="errors.email" class="error"></p>

    <input type="password" x-model="password" placeholder="Password">
    <p x-show="errors.password" x-text="errors.password" class="error"></p>

    <button type="submit">Submit</button>
  </form>
</div>

Alpine.js with Components

For reusable logic, extract into Alpine components:

<script>
  document.addEventListener('alpine:init', () => {
    Alpine.data('dropdown', () => ({
      open: false,
      toggle() {
        this.open = !this.open;
      },
      close() {
        this.open = false;
      }
    }));
  });
</script>

<div x-data="dropdown" @click.away="close()">
  <button @click="toggle()">Menu</button>
  <div x-show="open">
    <a href="#">Item 1</a>
    <a href="#">Item 2</a>
  </div>
</div>

HTMX

HTMX extends HTML with attributes that let you access modern browser features like AJAX, CSS Transitions, WebSockets, and Server-Sent Events directly from HTML, without writing JavaScript.

Installing HTMX

HTMX is equally simple to set up. Like Alpine.js, it works directly in the browser via a CDN.

Include HTMX:

Add this script tag to your HTML file:

<script src="https://unpkg.com/htmx.org@1.9.10"></script>

This is all you need to start using HTMX attributes in your HTML.

Core HTMX Attributes

hx-get / hx-post / hx-put / hx-delete:

Make HTTP requests.

<button hx-get="/api/data" hx-target="#result">Load Data</button>

hx-target:

Specifies which element to update with the response.

<button hx-get="/data" hx-target="#content">Load</button>
<div id="content"></div>

hx-swap:

Determines how the response is inserted:

  • innerHTML (default) - Replace inner HTML
  • outerHTML - Replace entire element
  • beforebegin - Insert before element
  • afterbegin - Insert at start of element
  • beforeend - Insert at end of element
  • afterend - Insert after element
  • delete - Delete the target element
  • none - Don’t insert anything

hx-trigger:

Specifies what event triggers the request.

<input hx-get="/search" hx-trigger="keyup changed delay:500ms" hx-target="#results">

hx-indicator:

Shows a loading indicator during the request.

<button hx-get="/data" hx-indicator="#spinner">Load</button>
<div id="spinner" class="htmx-indicator">Loading...</div>

Basic HTMX Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>HTMX Example</title>
  <script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
  <button hx-get="/hello.html" hx-target="#content" hx-swap="innerHTML">
    Load Content
  </button>
  <div id="content"></div>
</body>
</html>

hello.html:

<h1>Hello, World!</h1>
<p>This content was loaded via HTMX!</p>

In this example:

  • hx-get sends a GET request to /hello.html
  • hx-target specifies which element to update (#content)
  • hx-swap determines how the response is inserted (replaces inner HTML)

The server response can be plain HTML fragments.

Practical HTMX Examples

Live search:

<input 
  type="search" 
  name="q"
  hx-get="/search" 
  hx-trigger="keyup changed delay:300ms"
  hx-target="#search-results"
  placeholder="Search..."
>
<div id="search-results"></div>

Server response (/search?q=term):

<div class="results">
  <div class="result">Result 1</div>
  <div class="result">Result 2</div>
</div>

Delete item with confirmation:

<button 
  hx-delete="/api/items/123"
  hx-confirm="Are you sure you want to delete this item?"
  hx-target="closest .item"
  hx-swap="outerHTML"
>
  Delete
</button>

Infinite scroll:

<div id="content">
  <!-- Initial items -->
  <div class="item">Item 1</div>
  <div class="item">Item 2</div>

  <!-- Load more trigger -->
  <div 
    hx-get="/api/items?page=2" 
    hx-trigger="revealed"
    hx-swap="afterend"
  >
    <span class="loading">Loading more...</span>
  </div>
</div>

Form submission:

<form hx-post="/api/contact" hx-target="#form-result">
  <input type="text" name="name" placeholder="Name" required>
  <input type="email" name="email" placeholder="Email" required>
  <textarea name="message" placeholder="Message" required></textarea>
  <button type="submit">Send</button>
</form>
<div id="form-result"></div>

Server success response:

<div class="success">Thank you! Your message has been sent.</div>

Server error response (with 4xx or 5xx status):

<div class="error">Please fix the errors and try again.</div>

Polling for updates:

<div 
  hx-get="/api/notifications" 
  hx-trigger="every 5s"
  hx-target="this"
  hx-swap="innerHTML"
>
  <!-- Notifications will be updated every 5 seconds -->
</div>

File upload with progress:

<form hx-post="/upload" hx-encoding="multipart/form-data">
  <input type="file" name="file">
  <button type="submit">Upload</button>
  <div class="htmx-indicator">
    <progress></progress>
  </div>
</form>

Combining Alpine.js and HTMX

Alpine.js and HTMX work great together:

HTMX for server interactions, Alpine for UI:

<div x-data="{ showDetails: false }">
  <!-- HTMX loads content from server -->
  <button 
    hx-get="/user/123" 
    hx-target="#user-data"
    @click="showDetails = true"
  >
    Load User
  </button>

  <!-- Alpine controls visibility -->
  <div id="user-data" x-show="showDetails"></div>
  <button @click="showDetails = false" x-show="showDetails">Hide</button>
</div>

Dynamic tabs with server-loaded content:

<div x-data="{ activeTab: 'home' }">
  <div class="tabs">
    <button 
      @click="activeTab = 'home'"
      hx-get="/tabs/home"
      hx-target="#tab-content"
      :class="{ 'active': activeTab === 'home' }"
    >
      Home
    </button>
    <button 
      @click="activeTab = 'profile'"
      hx-get="/tabs/profile"
      hx-target="#tab-content"
      :class="{ 'active': activeTab === 'profile' }"
    >
      Profile
    </button>
  </div>
  <div id="tab-content"></div>
</div>

Comparison with Other Solutions

Feature Alpine.js HTMX jQuery React Vue
Bundle Size ~15KB ~14KB ~30KB ~170KB ~40KB
Build Required Optional
Learning Curve Easy Easy Easy Steep Medium
State Management Simple Server Manual Complex Built-in
Server Interaction Manual Built-in Manual Manual Manual
Best For UI interactions Server-driven Legacy SPAs SPAs

Backend Setup for HTMX

HTMX works with any backend that can serve HTML. Here are examples:

Node.js/Express:

app.get('/hello.html', (req, res) => {
  res.send('<h1>Hello from Express!</h1>');
});

app.post('/api/contact', (req, res) => {
  // Process form
  res.send('<div class="success">Message sent!</div>');
});

Python/Flask:

@app.route('/hello.html')
def hello():
    return '<h1>Hello from Flask!</h1>'

@app.route('/api/contact', methods=['POST'])
def contact():
    # Process form
    return '<div class="success">Message sent!</div>'

PHP:

<?php
// hello.php
echo '<h1>Hello from PHP!</h1>';

// contact.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Process form
    echo '<div class="success">Message sent!</div>';
}

Development Tips

Alpine.js tips:

  • Keep components small and focused
  • Use Alpine.data() for reusable components
  • Use x-cloak to prevent flash of unstyled content
  • Leverage @click.away for dropdowns and modals
  • Use x-if for expensive DOM operations, x-show for simple toggling

HTMX tips:

  • Return HTML fragments, not full pages
  • Use appropriate HTTP status codes (200, 400, 500, etc.)
  • Leverage hx-swap for different insertion behaviors
  • Use hx-push-url to update browser history
  • Add hx-indicator for better UX during requests
  • Use hx-trigger modifiers like changed, delay, throttle

Both:

  • Start simple - add complexity only when needed
  • Use browser DevTools to debug
  • Read the documentation - both have excellent docs
  • Combine with Tailwind CSS for rapid styling

Summary

Alpine.js and HTMX offer lightweight, no-build-step solutions for adding interactivity to web applications, each with a different philosophy.

Key Takeaways

Alpine.js:

  • Lightweight reactive framework (~15KB)
  • Vue-like syntax without build step
  • Perfect for UI interactions (dropdowns, modals, tabs)
  • Client-side state management
  • Works directly via CDN
  • “jQuery for the modern web”

HTMX:

  • HTML-first approach (~14KB)
  • Makes AJAX requests via HTML attributes
  • Server-driven interactions
  • Progressive enhancement
  • Works with any backend
  • “Access modern browser features via HTML”

When to Use:

  • Alpine.js: Client-side interactivity, reactive components
  • HTMX: Server-rendered apps, traditional backends
  • Both together: Hybrid approach - UI with Alpine, data with HTMX
  • React/Vue: Complex SPAs, large teams, advanced state management

Advantages:

  • No build system required
  • Small bundle size
  • Easy learning curve
  • Progressive enhancement
  • Works with existing HTML
  • CDN-ready

Best Practices:

  • Keep Alpine components small and focused
  • Return HTML fragments from HTMX endpoints
  • Use appropriate HTTP methods and status codes
  • Combine with Tailwind CSS for styling
  • Start simple, add complexity as needed

Backend Compatibility:

  • Works with any server that returns HTML
  • Node.js, Python, PHP, Ruby, Go, .NET, etc.
  • Perfect for traditional server-rendered applications
  • No API serialization overhead

Comparison to Frameworks:

  • Much lighter than React/Vue (15KB vs 170KB)
  • No build step unlike modern frameworks
  • Simpler than jQuery with modern syntax
  • Different philosophy - enhancement vs replacement

Alpine.js and HTMX represent a modern approach to web development that embraces HTML and progressive enhancement while avoiding the complexity of large frameworks and build systems. They’re excellent choices for projects that want modern interactivity without the overhead.