Build fast, hypermedia-driven web apps with Dart + htmx
Index
Introduction
Htmdart was born out of a simple need: to move fast when building hypermedia-driven web applications in Dart. While frameworks like shelf
(or newer ones like relic
) provide excellent foundations, they often lack certain conveniences that make everyday development smooth.
That’s why Htmdart gives you:
- Direct HTML rendering in Dart - powered by the separate htmleez package, so you can compose markup anywhere.
- Attributes for htmx & hyperscript - all the
hx-*
and_
attributes available as htmleez attributes. - A router built for speed & DX - radix-tree based, with grouping, middlewares, static file serving, and redirects included out of the box.
- A custom
serve
function - wrappingshelf_io.serve
, with extras like hot-reload and opinionated defaults.
These tools are not meant to reinvent the Dart web ecosystem. They exist because, today, the existing solutions aren’t yet at the level where building a complete hypermedia app feels frictionless. Htmdart fills that gap.
Looking Ahead
The long-term vision of Htmdart is actually to become smaller, not bigger.
- The essential piece is attribute helpers (htmx, hyperscript, etc.) - those are stable and unlikely to change.
- Routers, serve utilities, or other abstractions may be deprecated in the future, once Dart’s web frameworks mature enough to cover these features well.
In short: Htmdart is a bridge - giving you the speed and ergonomics you need right now, while leaving room for you to adopt other frameworks as they evolve.
Hello World Example
import "dart:io";
import "dart:math";
import 'package:htmdart/htmdart.dart';
final router = Router()
..get("/", homePage)
..get("/random", randomNumber);
void main() => serve(router, InternetAddress.anyIPv4, 8080, withHotreload: false);
Response homePage(Request _) => respondWithHtml([
html([
body([
div([$id("number"), "0".t]),
button([
$hx.get("/random"),
"Get Random Number".t,
]),
]),
]),
]);
Response randomNumber(Request _) => respondWithHtmlOob([
div([
$id("number"),
Random().nextInt(100).toString().t,
]),
]);
Core Concepts
1 HTMX utilities
The hx
class provides all the standard HTMX attributes
- Simple verbs
hx.get("/path") hx.post("/submit") hx.put(...) hx.delete(...)
- Dynamic handler binding
// Let's imagine this handler Response handleNoteDetails(Request req, String noteId) {...} final router = Router()..get("/notes/<noteId>", handleNoteDetails); // Then in your elements you can automatically pick up the HTTP verb and route $hx.handle(handleNoteDetails) // In case your route has path parameters (like in this case) // you can pass them in order as a List<String> $hx.handler(handleNoteDetails, [note.id]) // You can also pass query parameters $hx.handler(handleNoteDetails, [note.id], {"fromQP": true}) // It renders to // hx-get="/notes/myNoteId?fromQP=true";
- Swap controls
$hx.swap.innerHTML $hx.swapOob.yes // out-of-band swaps
- Extras
All the available hx attributes from htmx have been added, you can see them here$hx.vals("js:{ count: … }") // inject JSON values $hx.select(".result") // response selector $hx.confirm("Are you sure?")
2 Hyperscript attribute
Built-in support for hyperscript's attribute
div([
className("btn"),
$_("on click add .active to me then wait 500ms then remove .active"),
"Click me".t,
])
3 Router
final router = Router()
..notFoundHandler((req) => html([h1(["404".t])]).response)
..get("/", homePageHandler)
..post("/increment", incrementHandler);
void main() async {
final server = await io.serve(router.call, 'localhost', 8080);
print("Listening on ${server.port}");
}
- Path parameters
Response handler(Request req, String param) {...} final router = Router()..get("/handler/<param>", handler);
- Any Method
Response handler(Request req, String param) { switch(req.method) { case "GET": ... case "POST": ... } } final router = Router()..any("/any", handler);
- Grouping
final apiGroup = router.group("/api"); apiGroup.get("/items", listItems); apiGroup.post("/items", createItem);
- Static files
router.static("/public", "web/public");
- Middleware
- Built-in support for middlewares with
router.use(myMiddleware)
- Built-in support for middlewares with
- Redirects
Redirect not defined routes with
router.redirect("/", "/login"),
4. Helpers
-
Response shortcuts
respondWithHtml([...])
: wraps a list ofHTML
fragments into a fullResponse
with the correctContent-Type
.respondWithHtmlOob([...])
: same as above, but optimized for out-of-band swaps in htmx (adds$hx.swapOob.yes
to all the fragments).
-
Request extensions
On anyRequest
you can check htmx metadata:if (request.isHx) { ... } // true if HX-Request == "true" print(request.hxTarget); // target element print(request.hxTriggerName); // trigger name
Libraries
- htmdart
- Htmdart