Skip to main content

Command Palette

Search for a command to run...

hctx — The Missing Client-Side Layer for HTMX

Updated
5 min read

I’ve been building with HTMX for a while now, and I genuinely love the model:

  • Server-driven UI

  • HTML over the wire

  • Minimal JavaScript

  • No heavy frontend build pipeline

It feels clean. Predictable. Refreshing.

But eventually, I kept running into the same problem.

Not a big one.

Just… small things.

  • A counter

  • A toggle

  • Client-side validation before submit

  • A temporary UI state

  • A small interactive widget

Not enough to justify React.
Too structured for random vanilla JS event listeners scattered everywhere.

So I built hctx.


The Problem: “Just a Little State”

HTMX intentionally avoids managing client-side state. That’s part of its philosophy.

But real applications often need some client logic.

And when that happens, you usually end up with one of these:

  1. Inline <script> blocks with DOM queries and event listeners

  2. Global variables floating around

  3. A growing mess of tiny utilities

  4. Or… pulling in a full framework just to feel organized

There’s a huge gap between:

“I just need a sprinkle of JS”
and
“Fine, I’ll install a full SPA framework.”

hctx lives in that gap.


What is hctx?

hctx is a tiny (~5KB gzipped) declarative layer that adds structured client-side behavior directly through HTML attributes.

It’s designed specifically to sit alongside HTMX.

The core idea is simple:

  • Actions mutate state

  • Effects update the DOM

  • HTML wires everything together

That’s it.


A Simple Example

Here’s a counter:

<div hctx="counter">
  <span hc-effect="render on hc:statechanged">0</span>
  <button hc-action="increment on click">+1</button>
  <button hc-action="decrement on click">-1</button>
</div>

<script>
hctx.newCtx("counter", () => ({
  data: { count: 0 },
  actions: {
    increment: ({ data }) => { data.count++; },
    decrement: ({ data }) => { data.count--; }
  },
  effects: {
    render: {
      handle: ({ data, el }) => { el.textContent = data.count; },
      subscribe: ({ add, data }) => { add(data, "count"); }
    }
  }
}));
</script>

What’s happening?

  • data holds state.

  • actions mutate it.

  • effects react to changes and update the DOM.

  • HTML binds user interactions declaratively:

hc-action="increment on click"
hc-effect="render on hc:statechanged"

No component files.
No build tools required.
No runtime overhead beyond ~5KB.


Why Not Just Use Vanilla JS?

You can.

But once you have more than one interactive widget, you’ll notice patterns emerging:

  • State needs structure.

  • DOM updates need to be reactive.

  • Cleanup becomes important.

  • Cross-component communication appears.

  • Async logic creeps in.

hctx gives you:

  • A predictable structure

  • Explicit separation between mutation and DOM updates

  • Scoped contexts

  • Automatic cleanup

  • Optional middleware and stores

Without turning your project into a framework-driven architecture.


Designed for HTMX

HTMX swaps fragments into the DOM.

That means your client logic must handle dynamic insertion.

hctx uses a MutationObserver to automatically wire newly inserted elements.
So when HTMX swaps HTML into the page, hctx just works.

No manual re-initialization.

No lifecycle hacks.

Just declarative behavior.


Features at a Glance

  • ~5KB gzipped

  • Zero dependencies

  • Works via <script> tag (IIFE) or ES module

  • Full TypeScript support

    • Typed contexts

    • Typed stores

    • Typed middleware

  • Async actions

  • Cross-context communication

  • Fragments (same context across multiple DOM regions)

  • Write traps enforcing:

    • Actions mutate

    • Effects read

It scales when you need it to — but doesn’t force you to use everything.


What hctx Is Not

It’s not React.
It’s not a SPA framework.
It’s not trying to replace HTMX.

It’s for when you want:

  • Server-driven rendering

  • Minimal JS

  • But still need structured client state

Think of it as:

Alpine.js-level simplicity

  • A stricter action/effect mental model

  • Explicit state discipline

  • Designed for HTMX from day one


When Should You Use It?

Use hctx when:

  • You’re building HTMX apps

  • You want client-side behavior without framework overhead

  • You care about bundle size

  • You prefer HTML-first architecture

  • You want structure without complexity

Don’t use it if:

  • You’re building a complex SPA with client routing

  • You need full client-side rendering

  • You want a large ecosystem of UI components

Different tools for different jobs.


Installation

npm

npm install hctx

CDN

<script src="https://unpkg.com/hctx"></script>

Repo & docs:
https://github.com/aggroot/hctx


Why I Built It

I didn’t want to choose between:

  • Messy vanilla JS
    or

  • Heavy frameworks

I wanted something tiny, intentional, and disciplined.

Something that keeps the HTMX philosophy intact.

hctx is my attempt at that.

It’s still early (v0.1.0), and I’m actively refining it.

If you’re building HTMX apps and feel the same friction, I’d genuinely love your feedback.

What would make this useful for you?