Web Development

HTMX: Dynamic UI Tanpa JavaScript

Bangun antarmuka web yang interaktif dan dinamis hanya dengan atribut HTML — tanpa perlu menulis satu baris JavaScript pun. Pelajari atribut, triggers, AJAX, boosting, SSE, WebSockets, dan templates.

1. Pengenalan HTMX

HTMX adalah library ringan yang memungkinkan Anda membangun UI dinamis langsung dari HTML, tanpa perlu menulis JavaScript. HTMX memperluas kemampuan HTML standar sehingga elemen-elemen biasa seperti <button>, <div>, atau <form> bisa melakukan permintaan AJAX, mengganti konten halaman, dan berinteraksi dengan server secara real-time.

Filosofi HTMX berakar pada hypermedia — konsep asli web di mana HTML adalah API utama. Alih-alih membangun Single Page Application (SPA) dengan framework JavaScript berat, HTMX mengembalikan pendekatan server-centric yang lebih sederhana.

Mengapa HTMX?

KeunggulanPenjelasanContoh
SederhanaHanya butuh atribut HTML, tanpa JS<button hx-get="/data">
Ringan~14KB minified, tanpa dependencyCocok untuk website performa tinggi
KompatibelBekerja dengan bahasa backend apapunPython, Go, PHP, Rust, Node.js
Progressive EnhancementTetap berfungsi tanpa JavaScriptGraceful degradation alami
HATEOASImplementasi arsitektur hypermedia RESTServer mengembalikan HTML, bukan JSON

Instalasi HTMX

HTML — Install HTMX via CDN
<!-- CDN (paling mudah) -->
<head>
  <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>

<!-- Atau via npm -->
<!-- npm install htmx.org -->
<!-- import 'htmx.org'; -->
HTML — Contoh Sederhana Pertama
<!-- Ketika tombol diklik, isi #result diganti dengan response dari /api/greeting -->
<button hx-get="/api/greeting" hx-target="#result" hx-swap="innerHTML">
  Dapatkan Salam
</button>

<div id="result">
  <!-- Response dari server akan muncul di sini -->
</div>
💡 Tips

HTMX tidak menggantikan JavaScript sepenuhnya — untuk interaksi kompleks seperti drag-and-drop, animasi rumit, atau manipulasi data klien, JavaScript tetap diperlukan. HTMX ideal untuk operasi CRUD, navigasi, dan pembaruan UI berbasis server.

2. Atribut Dasar HTMX

HTMX bekerja melalui atribut-atribut spesifik yang ditambahkan ke elemen HTML. Berikut atribut-atribut inti yang harus Anda kuasai:

Atribut Permintaan (Request Attributes)

AtributFungsiContoh
hx-getMelakukan HTTP GEThx-get="/api/users"
hx-postMelakukan HTTP POSThx-post="/api/users"
hx-putMelakukan HTTP PUThx-put="/api/users/1"
hx-patchMelakukan HTTP PATCHhx-patch="/api/users/1"
hx-deleteMelakukan HTTP DELETEhx-delete="/api/users/1"

Atribut Target & Swap

HTML — Target dan Swap
<!-- hx-target: elemen mana yang akan di-update -->
<!-- hx-swap: bagaimana konten di-swap -->

<!-- Replace innerHTML (default) -->
<button hx-get="/data" hx-target="#output" hx-swap="innerHTML">
  Load Data
</button>

<!-- Replace seluruh elemen -->
<button hx-get="/data" hx-target="#output" hx-swap="outerHTML">
  Load & Replace
</button>

<!-- Insert sebelum elemen target -->
<button hx-get="/item" hx-target="#list" hx-swap="beforebegin">
  Tambah di Atas
</button>

<!-- Insert setelah elemen target -->
<button hx-get="/item" hx-target="#list" hx-swap="afterend">
  Tambah di Bawah
</button>

<!-- Swap dengan animasi -->
<button hx-get="/data" hx-target="#output" hx-swap="innerHTML settle:500ms">
  Load dengan Delay
</button>

<div id="output"></div>
<ul id="list">
  <li>Item 1</li>
</ul>

Atribut Sinkronisasi & Indikator

HTML — Loading Indicator
<!-- hx-indicator: menampilkan loading spinner saat request berlangsung -->
<button hx-get="/api/slow-data" hx-target="#data" hx-indicator="#spinner">
  Load Data Lambat
</button>

<div id="spinner" class="htmx-indicator">
  ⏳ Loading...
</div>

<div id="data"></div>

<style>
  /* Sembunyikan indicator secara default */
  .htmx-indicator {
    display: none;
  }
  /* Tampilkan saat HTMX sedang request */
  .htmx-request .htmx-indicator {
    display: inline-block;
  }
  .htmx-request.htmx-indicator {
    display: inline-block;
  }
</style>

3. Triggers & Event Handling

Secara default, HTMX memicu permintaan saat elemen diklik (untuk button/link) atau di-submit (untuk form). Namun Anda bisa mengkustomisasi trigger menggunakan atribut hx-trigger.

HTML — Berbagai Macam Trigger
<!-- Trigger saat mouseover -->
<div hx-get="/tooltip" hx-trigger="mouseenter" hx-target="#tooltip-box">
  Hover saya untuk tooltip
</div>

<!-- Trigger saat pengguna berhenti mengetik (debounce) -->
<input type="text"
       hx-get="/api/search"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#search-results"
       placeholder="Cari...">

<!-- Trigger setiap 5 detik (polling) -->
<div hx-get="/api/live-feed" hx-trigger="every 5s" hx-target="#feed">
  <div id="feed">Live feed akan update otomatis...</div>
</div>

<!-- Trigger saat elemen terlihat di viewport -->
<div hx-get="/api/more-content" hx-trigger="revealed" hx-target="#infinite-list">
  <ul id="infinite-list"></ul>
</div>

<!-- Multiple triggers -->
<input type="text"
       hx-get="/api/validate"
       hx-trigger="keyup changed delay:300ms, blur"
       hx-target="#validation-msg">

<!-- Custom event -->
<div hx-get="/api/data" hx-trigger="myCustomEvent" hx-target="#data">
  Data container
</div>

<button onclick="htmx.trigger('[hx-trigger*=myCustomEvent]', 'myCustomEvent')">
  Trigger Custom Event
</button>

Modifier Trigger

ModifierFungsiContoh
onceHanya trigger sekalihx-trigger="click once"
changedHanya jika nilai berubahhx-trigger="input changed"
delay:XXmsTunda sebelum triggerhx-trigger="keyup delay:500ms"
throttle:XXmsBatasi frekuensi triggerhx-trigger="scroll throttle:1s"
target:selectorTrigger dari elemen lainhx-trigger="click target:.btn"
consumeMencegah propagasi eventhx-trigger="click consume"
queue:first/last/allAtur antrian eventhx-trigger="input queue:all"

4. AJAX dengan HTMX

HTMX mempermudah AJAX request dengan hanya menambah atribut pada elemen HTML. Server mengembalikan fragmen HTML, bukan JSON, yang langsung di-insert ke DOM.

Form Submission

HTML — Form dengan HTMX
<!-- Form biasa yang di-submit via AJAX -->
<form hx-post="/api/contact" hx-target="#form-result" hx-swap="innerHTML">
  <label for="name">Nama:</label>
  <input type="text" id="name" name="name" required>

  <label for="email">Email:</label>
  <input type="email" id="email" name="email" required>

  <label for="message">Pesan:</label>
  <textarea id="message" name="message" rows="4" required></textarea>

  <button type="submit">Kirim</button>
</form>

<div id="form-result"></div>
Python (Flask) — Server Response
from flask import Flask, request

app = Flask(__name__)

@app.route('/api/contact', methods=['POST'])
def contact():
    name = request.form.get('name')
    email = request.form.get('email')
    message = request.form.get('message')

    # Simpan ke database...
    save_contact(name, email, message)

    # Kembalikan fragmen HTML (bukan JSON!)
    return '''
    <div class="success-message">
      <h3>✅ Pesan Terkirim!</h3>
      <p>Terima kasih, {name}! Kami akan membalas ke {email}.</p>
    </div>
    '''.format(name=name, email=email)

Headers & Parameters

HTML — Mengirim Data Tambahan
<!-- hx-vals: mengirim data tambahan dengan setiap request -->
<button hx-post="/api/action"
        hx-vals='{"role": "admin", "page": 1}'
        hx-target="#result">
  Kirim dengan Data Ekstra
</button>

<!-- hx-headers: mengirim custom headers -->
<button hx-get="/api/protected"
        hx-headers='{"Authorization": "Bearer mytoken123"}'
        hx-target="#data">
  Akses Endpoint Terproteksi
</button>

<!-- hx-include: sertakan nilai dari elemen lain -->
<input type="hidden" name="token" value="abc123" id="csrf-token">
<button hx-post="/api/action"
        hx-include="#csrf-token"
        hx-target="#result">
  Kirim dengan CSRF Token
</button>

<!-- hx-params: filter parameter yang dikirim -->
<form hx-post="/api/search" hx-params="*" hx-target="#results">
  <input type="text" name="q">
  <input type="hidden" name="source" value="web">
  <button type="submit">Cari</button>
</form>

Error Handling

HTML — Response Target Berdasarkan Kode
<!-- hx-target-*: target berdasarkan status code -->
<form hx-post="/api/users"
      hx-target="#success-msg"
      hx-target-422="#error-msg"
      hx-swap="innerHTML">
  <input type="text" name="username" required>
  <button type="submit">Daftar</button>
</form>

<div id="success-msg"></div>
<div id="error-msg"></div>
JavaScript — Custom Event Error Handler
// Menangani response error secara global
document.body.addEventListener('htmx:responseError', function(evt) {
  const xhr = evt.detail.xhr;
  const status = xhr.status;

  switch(status) {
    case 404:
      alert('Resource tidak ditemukan (404)');
      break;
    case 422:
      console.log('Validation error:', xhr.responseText);
      break;
    case 500:
      alert('Server error. Coba lagi nanti.');
      break;
    default:
      alert('Error: ' + status);
  }
});

// Menggunakan htmx:configRequest untuk manipulasi request
document.body.addEventListener('htmx:configRequest', function(evt) {
  evt.detail.headers['X-CSRF-Token'] = getCsrfToken();
});

5. HTMX Boosting

HTMX Boosting memungkinkan Anda meng-upgrade seluruh link dan form di halaman agar menggunakan AJAX secara otomatis, tanpa menambah atribut HTMX pada setiap elemen satu per satu.

HTML — Boosting pada Elemen
<!-- Boost link individu -->
<a href="/about" hx-boost="true">Tentang Kami</a>

<!-- Boost seluruh section -->
<main hx-boost="true">
  <a href="/page1">Halaman 1</a>      <!-- Akan di-load via AJAX -->
  <a href="/page2">Halaman 2</a>      <!-- Akan di-load via AJAX -->
  <a href="/page3">Halaman 3</a>      <!-- Akan di-load via AJAX -->

  <form action="/submit" method="POST"> <!-- Juga via AJAX -->
    <input name="data">
    <button type="submit">Submit</button>
  </form>
</main>

<!-- Boost seluruh body (semua link & form di halaman) -->
<body hx-boost="true">
  ...
</body>
HTML — Menggunakan hx-push-url
<!-- Push URL baru ke browser history -->
<a href="/new-page" hx-get="/new-page" hx-target="#content"
   hx-push-url="true">
  Navigasi dengan History Push
</a>

<!-- Mengubah URL tanpa push (replace) -->
<div hx-get="/tab-content" hx-trigger="click"
     hx-push-url="false" hx-target="#tab-body">
  Tab Content
</div>
⚠️ Perhatian

Ketika menggunakan hx-boost, link eksternal dan anchor links (#) tidak akan terpengaruh. HTMX secara otomatis mengabaikan link yang menargetkan domain berbeda atau link fragment.

6. Server-Sent Events (SSE)

Server-Sent Events (SSE) memungkinkan server mengirim update real-time ke klien. HTMX mendukung SSE melalui ekstensi hx-ext="sse".

HTML — SSE dengan HTMX Extension
<!-- Load HTMX SSE extension -->
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>

<!-- Membuka koneksi SSE dan mengikuti event -->
<div hx-ext="sse" sse-connect="/api/stream" sse-swap="message">
  <!-- Konten dari event "message" akan di-swap ke sini -->
  Menunggu data dari server...
</div>

<!-- Mengikuti beberapa event -->
<div hx-ext="sse" sse-connect="/api/events">
  <div sse-swap="notification">
    <!-- Event "notification" -->
  </div>
  <div sse-swap="status-update">
    <!-- Event "status-update" -->
  </div>
  <div sse-swap="chat-message">
    <!-- Event "chat-message" -->
  </div>
</div>

<!-- Trigger HTMX request berdasarkan SSE event -->
<div hx-ext="sse" sse-connect="/api/events">
  <button hx-get="/api/stats" hx-trigger="sse:refresh-stats" hx-target="#stats">
    Stats (akan auto-refresh saat event "refresh-stats")
  </button>
  <div id="stats"></div>
</div>
Node.js (Express) — SSE Server
const express = require('express');
const app = express();

app.get('/api/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  let counter = 0;
  const interval = setInterval(() => {
    counter++;
    // Format SSE: "event: <nama>\ndata: <isi>\n\n"
    res.write(`event: message\ndata: <div class="update">Update #${counter} pada ${new Date().toLocaleTimeString()}</div>\n\n`);
  }, 3000);

  req.on('close', () => {
    clearInterval(interval);
  });
});

app.listen(3000, () => console.log('SSE server running on port 3000'));

7. WebSockets dengan HTMX

Untuk komunikasi dua arah penuh (bidirectional), HTMX mendukung WebSockets melalui ekstensi. Berbeda dari SSE yang hanya satu arah (server → klien), WebSockets memungkinkan klien mengirim pesan ke server secara real-time.

HTML — WebSocket dengan HTMX
<!-- Load HTMX WebSocket extension -->
<script src="https://unpkg.com/htmx-ext-ws@2.0.2/ws.js"></script>

<!-- Koneksi WebSocket dengan send dan receive -->
<div hx-ext="ws" ws-connect="/ws/chat">
  <!-- Messages dari server akan masuk ke #chatroom -->
  <div id="chatroom" style="height:400px;overflow-y:auto;border:1px solid #333;padding:1rem;">
    <p>Selamat datang di chat!</p>
  </div>

  <!-- Form ini mengirim data ke WebSocket saat submit -->
  <form ws-send>
    <input type="text" name="message" placeholder="Ketik pesan..." required>
    <button type="submit">Kirim</button>
  </form>
</div>

<!-- Mengirim data spesifik ke WebSocket -->
<button hx-ext="ws" ws-connect="/ws/notifications"
        ws-send='{"action": "subscribe", "channel": "updates"}'>
  Subscribe ke Updates
</button>
Node.js (ws) — WebSocket Server
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);
  console.log('Client terhubung. Total:', clients.size);

  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    // Format: server mengembalikan HTML fragmen
    const html = `<div class="chat-message">
      <strong>User:</strong> ${msg.message}
      <span class="time">${new Date().toLocaleTimeString()}</span>
    </div>`;

    // Broadcast ke semua client
    for (const client of clients) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(html);
      }
    }
  });

  ws.on('close', () => {
    clients.delete(ws);
  });
});

console.log('WebSocket server berjalan di ws://localhost:8080');

8. Templates & Morphing

HTMX mendukung fitur template untuk menampilkan konten sementara (loading, error) dan morphing untuk transisi halaman yang mulus.

Content Morphing (Idiomorph)

HTML — Idiomorph Extension
<!-- Load Idiomorph extension untuk morphing -->
<script src="https://unpkg.com/htmx-ext morph@0.0.4/morph.js"></script>

<!-- Menggunakan morphing alih-alih swap biasa -->
<body hx-ext="morph">
  <main hx-get="/new-content" hx-trigger="click" hx-target="#app"
        hx-swap="morph">
    <div id="app">
      <h1>Halaman Awal</h1>
      <p>Konten akan di-morph secara mulus</p>
    </div>
  </main>
</body>

View Transitions dengan HTMX

JavaScript — Integrasi View Transitions
// Menggunakan View Transitions API bersama HTMX
document.addEventListener('htmx:beforeSwap', function(evt) {
  if (document.startViewTransition) {
    evt.preventDefault();
    document.startViewTransition(() => {
      evt.detail.shouldSwap = true;
      htmx.swap(evt.detail.target, evt.detail.content, evt.detail);
    });
  }
});

// Mengatur transition name pada elemen
document.addEventListener('htmx:beforeSwap', function(evt) {
  const target = evt.detail.target;
  target.style.viewTransitionName = 'main-content';
});

Kombinasi HTMX dengan Alpine.js

HTML — HTMX + Alpine.js
<!-- HTMX untuk server interaction, Alpine.js untuk client state -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x"></script>

<div x-data="{ showModal: false, selectedUser: null }">
  <!-- HTMX load user list dari server -->
  <div hx-get="/api/users" hx-trigger="load" hx-target="#user-list">
    <ul id="user-list">Loading users...</ul>
  </div>

  <!-- Modal dikelola oleh Alpine.js (client-side state) -->
  <template x-if="showModal">
    <div class="modal-overlay" @click.self="showModal = false">
      <div class="modal">
        <!-- Detail user di-load dari server via HTMX -->
        <div hx-get="'/api/users/' + selectedUser"
             hx-trigger="load"
             hx-target="#user-detail">
          <div id="user-detail">Loading...</div>
        </div>
        <button @click="showModal = false">Tutup</button>
      </div>
    </div>
  </template>
</div>

9. Best Practices

✅ Checklist HTMX Best Practices
  • Gunakan hx-boost untuk progressive enhancement tanpa rewrite
  • Server mengembalikan HTML — bukan JSON. HTMX memperkuat arsitektur server-centric
  • Debounce input — selalu gunakan delay: pada trigger search/input
  • Gunakan OOB Swaps (hx-swap-oob="true") untuk update beberapa bagian halaman sekaligus
  • Manfaatkan CSS transitions dengan class htmx-settling untuk animasi
  • Error handling — selalu tangani response error dengan hx-target-4xx dan htmx:responseError
  • CSRF Protection — gunakan hx-headers atau htmx:configRequest untuk token
  • Audit request — pantau jumlah request yang dikirim (terutama polling) untuk efisiensi
  • Kombinasikan dengan Alpine.js atau Hyperscript untuk client-side state
  • Test tanpa JavaScript — pastikan website tetap berfungsi (progressive enhancement)
HTML — OOB Swap (Out of Band)
<!-- Request pertama: update target utama -->
<form hx-post="/api/cart/add" hx-target="#cart-items">
  <input type="hidden" name="product_id" value="42">
  <button type="submit">Tambah ke Keranjang</button>
</form>

<!-- Server mengembalikan: -->
<!-- 1. Fragmen untuk #cart-items (target utama) -->
<div>
  <li>Produk A - Rp 50.000</li>
  <li>Produk B - Rp 75.000</li>
</div>

<!-- 2. Fragmen OOB untuk update badge di tempat lain -->
<div id="cart-badge" hx-swap-oob="true">
  3 item
</div>

<!-- 3. Fragmen OOB untuk update total harga -->
<div id="cart-total" hx-swap-oob="true">
  Rp 175.000
</div>

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial HTMX, jawablah 5 pertanyaan berikut:

Pertanyaan 1: Apa atribut HTMX yang digunakan untuk menentukan elemen target yang akan di-update?

a) hx-swap
b) hx-target
c) hx-trigger
d) hx-get

Pertanyaan 2: Apa fungsi atribut hx-boost?

a) Mempercepat rendering halaman
b) Mengompresi respons server
c) Meng-upgrade link dan form agar menggunakan AJAX secara otomatis
d) Meng-cache hasil request

Pertanyaan 3: Apa yang dikembalikan oleh server saat menggunakan HTMX?

a) JSON response
b) XML response
c) Fragmen HTML
d) Binary data

Pertanyaan 4: Mana yang merupakan perbedaan utama antara SSE dan WebSockets?

a) SSE mendukung komunikasi dua arah
b) WebSockets hanya satu arah (server ke klien)
c) SSE satu arah (server ke klien), WebSockets dua arah
d) Tidak ada perbedaan

Pertanyaan 5: Apa yang dilakukan OOB Swap (hx-swap-oob)?

a) Menswap elemen target utama
b) Menswap elemen di luar target utama
c) Menolak swap
d) Menswap seluruh halaman
🔍 Zoom
100%
🎨 Tema