Building Interactive Sites with HTMX and Alpine.js on Vitto
Learn how to add dynamic interactions to your static site without a heavy JavaScript framework. Vitto makes it seamless to integrate HTMX and Alpine.js.
One of the best things about building with Vitto is the freedom to choose your tools. Unlike framework-locked SSGs, Vitto lets you reach for exactly what you need.
Two libraries that pair particularly well with static sites are HTMX and Alpine.js - and they work together beautifully.
Why HTMX and Alpine.js?
| Library | Purpose | Bundle Size |
|---|---|---|
| HTMX | Server-driven AJAX, form submissions, SSE | ~14KB min+gzip |
| Alpine.js | Client-side interactivity (toggles, tabs, modals) | ~16KB min+gzip |
| Together | Full interactivity without a framework | ~30KB total |
Tip
Both libraries work via HTML attributes. No virtual DOM, no build step, no JSX - just declarative HTML.
Setup
Install via npm:
npm install htmx.org alpinejs Then import in your main entry point:
// src/main.ts
import 'htmx.org';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start(); Or use CDN for quick prototyping:
{{ layout "layouts/base.vto" }}
<head>
{{ renderAssets() |> safe }}
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head> Examples
1. Dynamic Search with HTMX
Create a search endpoint and use HTMX to fetch results as the user types:
<!-- src/pages/index.vto -->
<div x-data="{ query: '' }">
<input
type="search"
x-model="query"
hx-get="/search-results"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
hx-include="[name='q']"
name="q"
placeholder="Search..."
class="px-4 py-2 border rounded-lg"
/>
<div id="results" class="mt-4">
<!-- HTMX injects results here -->
</div>
</div> 2. Interactive Tabs with Alpine.js
No JavaScript needed - just Alpine directives:
<div x-data="{ tab: 'features' }">
<nav class="flex gap-2">
<button @click="tab = 'features'" :class="tab === 'features' ? 'text-amber-600 border-b-2 border-amber-500' : ''">
Features
</button>
<button @click="tab = 'pricing'" :class="tab === 'pricing' ? 'text-amber-600 border-b-2 border-amber-500' : ''">
Pricing
</button>
<button @click="tab = 'about'" :class="tab === 'about' ? 'text-amber-600 border-b-2 border-amber-500' : ''">
About
</button>
</nav>
<div x-show="tab === 'features'" class="mt-4">
<h3>Features</h3>
<p>Content for features tab...</p>
</div>
<div x-show="tab === 'pricing'" class="mt-4">
<h3>Pricing</h3>
<p>Content for pricing tab...</p>
</div>
<div x-show="tab === 'about'" class="mt-4">
<h3>About</h3>
<p>Content for about tab...</p>
</div>
</div> 3. Lazy-Loaded Content with HTMX
Load content only when the user interacts:
<div
hx-get="/api/comments"
hx-trigger="click"
hx-swap="outerHTML"
class="cursor-pointer p-4 bg-sand-100 dark:bg-sand-800 rounded-lg"
>
Click to load comments
</div> 4. Form Submission without Page Reload
<form
hx-post="/api/subscribe"
hx-target="#subscribe-status"
hx-swap="innerHTML"
class="space-y-4"
>
<input type="email" name="email" placeholder="Your email" required
class="px-4 py-2 border rounded-lg w-full" />
<button type="submit"
class="px-6 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600">
Subscribe
</button>
</form>
<div id="subscribe-status"></div> Warning
HTMX requires a server endpoint for dynamic operations. For a fully static site, consider using serverless functions or a third-party service for forms and comments.
Combining Both Libraries
HTMX and Alpine.js complement each other perfectly. Use Alpine for UI state (open/close, active tab) and HTMX for server communication (fetch, submit, poll):
<div
x-data="{ open: false, loading: false }"
x-init="$watch('open', val => { if (val) loading = true })"
>
<button @click="open = !open" class="px-4 py-2 bg-sand-900 text-white rounded-lg">
<span x-show="!open">Show Details</span>
<span x-show="open">Hide</span>
</button>
<div x-show="open" x-transition class="mt-4">
<div hx-get="/api/details" hx-trigger="load" hx-swap="innerHTML">
<span x-show="loading">Loading...</span>
</div>
</div>
</div> When to Use Each Approach
- HTMX for: form submissions, search, pagination, content loading, server interactions
- Alpine.js for: toggles, tabs, modals, dropdowns, client-side validation
- Both for: complex UIs that need server interaction AND client state
Next Steps
Vitto’s template-based approach makes it easy to add interactivity exactly where you need it, without paying the cost of a full JavaScript framework everywhere else.
- HTMX Documentation
- Alpine.js Documentation
- Vitto Hooks System - Build API endpoints for HTMX