Getting started
Introduction
HTMX UI is a set of Tailwind CSS components for apps where the server renders HTML and htmx swaps it into the page.
Installation
Add the package to a new or existing Bun project.
Theming
Override design tokens or build a custom theme.
Dark mode
How dark mode works and how to add a switcher.
Components
Browse every component with live examples.
What it is#
Most component libraries assume a JavaScript framework owns the page. HTMX UI assumes the opposite: your server returns HTML, htmx swaps it in, and components need to look right and keep working when they arrive inside a fragment.
So every component is:
- Markup and classes. A button is
<button class="btn btn-primary">. Any server language can render it. - Styled with Tailwind v4. Component classes live in
@layer components, so a utility class on the same element always wins. - Themed with CSS variables. Colours and radius are variables like
--primaryand--radius, exposed to Tailwind asbg-primary,text-muted-foregroundand so on. - Behaviour only where needed. Things like copy buttons and the mobile sidebar are tiny TypeScript modules that re-run after every htmx swap.
A quick look#
Components compose like plain HTML. This card uses the card, badge and button components together:
Deploy preview
ReadyBuilt from main in 42 seconds.
<div class="card w-full max-w-sm">
<div class="card-header pb-6">
<div class="flex items-center justify-between">
<h3 class="card-title">Deploy preview</h3>
<span class="badge badge-success badge-dot">Ready</span>
</div>
<p class="card-description">Built from main in 42 seconds.</p>
</div>
<div class="card-footer pt-6">
<button class="btn btn-primary btn-sm">Open preview</button>
<button class="btn btn-ghost btn-sm">View logs</button>
</div>
</div>
Working with htmx#
The entry script calls initComponents(document) once the page loads, and again with
each newly inserted element on the htmx:after:process event. Each initialiser only touches
elements it hasn't seen yet (it marks them with data-init), so a fragment's
components come alive without anything running twice.
import "htmx.org";
import { initComponents } from "htmx-ui";
document.addEventListener("DOMContentLoaded", () => initComponents(document));
document.addEventListener("htmx:after:process", (e) => initComponents(e.target ?? document));
htmx:after:swap is dispatched on the element that made the request, so its target is not the new content.
htmx:after:process fires on each element htmx inserts, which is exactly what needs initialising.
What it's built on#
- htmx 4 for requests and swaps.
- Tailwind CSS v4 for styling and tokens.
- Bun for bundling, the dev server and tests.
- Nunjucks macros (optional) for less repetitive markup.