Frontend Without JS
- #frontend
- #js
- #serverless
- #flask
- #htmx
- #dev
Frontend for a backend dev, or how to build a reactive UI without React.
I like building backends and generally do not enjoy frontend. But if you’re making indie and pet projects, you won’t get very far without some kind of front. So how do you do it without learning React, while still building interactive interfaces? Ideally with no JavaScript at all. Or at least almost none.
We’re living in miraculous times. There are a few libraries now that completely cover my needs.
⚡ Alpine.js
This is modern jQuery. Very lightweight, no bundlers, no build tools. You include it from a CDN, and boom - you’ve got a reactive UI if you need one. In terms of capabilities, it’s closest to Vue, but much simpler.
<div x-data="{ open: false }">
<!--This button toggles open-->
<button @click="open = !open">
Toggle
</button>
<!--This block is shown when open = true-->
<div x-show="open">
Hello World
</div>
</div>
You can build proper components with it, or just hang it on dropdowns, menus, tabs, and modals. We’ve been using it on the main project for a long time, and so far we’ve never run into a reason to move to anything more serious. Quite the opposite: we originally had Vue.js and migrated away from it completely to Alpine. Less code, no build step, no dependency circus, the docs can be read in half an hour, everything is intuitive, and there is nothing extra.
Yes, sure, technically this is still JavaScript. But it’s so minimalist it barely counts. Most of the code is just HTML attributes and a couple of reactive variables. Everything you need for interactivity lives in x-data, x-show, @click, and similar directives.
🔌 Alpine plugins
Alpine has a few interesting plugins. These are the ones I found most charming.
👉 Persist
Stores state in localStorage or sessionStorage.
<div x-data="{ dark: $persist(false) }">
<button @click="dark = !dark">
Toggle Theme
</button>
</div>
Themes and user settings now survive page reloads. Civilized behavior at last.
👉 Sort
A plugin for drag-and-drop lists and card boards, basically Kanban-flavored rearranging.
<ul x-sort>
<li x-sort:item>foo</li>
<li x-sort:item>bar</li>
<li x-sort:item>baz</li>
</ul>
Items in such a list can be moved around with the mouse. Add a handler if you want the new state to be remembered.
👉 Collapse
A plugin for accordions and show/hide blocks with animation.
<div x-data="{ open: false }">
<button @click="open = !open">
Toggle
</button>
<div x-collapse x-show="open">
Hello World
</div>
</div>
🧠 HTMX
A complete game changer for minimalist frontend. It lets you make asynchronous requests without writing JavaScript. You just add the right attributes to the HTML:
<input hx-post="/search"
hx-trigger="keyup[key=='Enter']"
hx-target="#search-results"/>
<div id="search-results">
<!-- html from the response will appear here -->
</div>
It works with any backend, even PHP or Flask. It also supports WebSockets and SSE for real-time updates.
Now let’s see how this ties into the backend.
🔁 HTMX + Flask
The simplest possible integration: Flask backend.
@app.route("/hello")
def hello():
return "<p>Hello from Flask!</p>"
Now fetch that data straight into HTML:
<button
hx-get="/hello"
hx-target="#output">
Load
</button>
<div id="output">
<!-- The server response will appear here -->
</div>
Now let’s connect that with Alpine.
⚡ HTMX + Alpine + Flask
HTMX fetches HTML with Alpine inside it, and Alpine handles the logic from there.
<!-- Button loads a modal -->
<button
hx-get="/modal"
hx-target="#modal"
hx-swap="innerHTML">
Open Modal
</button>
<div id="modal">
<!-- Container where the Alpine component arrives -->
</div>
Flask returns a template:
@app.route('/modal')
def modal():
return render_template('modal.html')
Modal template with Alpine:
<!--modal.html-->
<div
x-data="{ open: true }"
x-show="open"
class="modal">
<p>Hello from modal</p>
<!-- Close the modal on click -->
<button @click="open = false">
Close
</button>
</div>
What about more complex requests? Like form submissions?
📋 Forms with HTMX
HTMX lets you submit forms without reloading the page and without JavaScript. Just add the right attributes:
<form
hx-post="/submit"
hx-target="#response">
<input type="text" name="name">
<button type="submit">
Submit
</button>
</form>
<div id="response">
<!-- The server response will appear here -->
</div>
Okay, but what if we have data and no form? For example, what if we need to send Alpine state to the backend?
📤 Sending Alpine data to the backend with HTMX
You can add hx-vals to a button or form to specify which data should be sent from Alpine:
<div x-data="{
name: 'Demon',
location: 'Bangkok'
}">
<button
hx-post="/submit"
:hx-vals="JSON.stringify({ name, location })">
Send data
</button>
</div>
Okay, not bad. There’s also Axios.
📦 Axios
Axios is an HTTP client for the browser and Node.js. It lets you make asynchronous API requests - yes, with actual JavaScript this time. If HTMX doesn’t fit, Axios is fine. Together with Alpine it can still stay very minimalist:
<div x-data="{ name: 'Demon' }">
<button @click="axios.post('/api/submit', { name })">
Send
</button>
</div>
Why Axios instead of the Fetch API? Because Axios is simpler to use. It handles JSON automatically and has a more pleasant syntax for dealing with responses.
And just like that, we now have everything we need to build a web app - and write almost no JS. And certainly not learn React. I am delighted.