Get started
Install htmxr, understand the four-step htmx cycle, and build your first HTML-over-the-wire app in R.
What is hyperverse?
htmx is a lightweight JavaScript library (~16kb) that lets any HTML element send HTTP requests, not just <a> and <form> tags.
The core philosophy is HTML over the wire: your server returns HTML fragments, not JSON. The browser swaps those fragments directly into the page without a full reload.
htmxr is the R wrapper: it provides htmltools-based primitives to generate htmx attributes and build complete pages, backed by a plumber2 server.
Installation
install.packages("htmxr")
# development version
pak::pak("hyperverse-r/htmxr")
htmxr uses plumber2 as its HTTP server. Make sure it is installed alongside htmxr.
How htmx works
Every htmx interaction follows the same four-step cycle:
- User triggers an event: a click, an input change, a page load, a form submission…
- htmx sends an HTTP request to your server (GET or POST)
- Your server returns an HTML fragment: a snippet of HTML, not JSON
- htmx swaps the fragment into the targeted DOM element
You control this cycle through five core attributes:
| Attribute | htmxr parameter | What it does |
|---|---|---|
hx-get | get = "/url" | Send GET request on trigger |
hx-post | post = "/url" | Send POST request on trigger |
hx-target | target = "#id" | CSS selector of the element to update |
hx-swap | swap = "innerHTML" | How to insert the response (innerHTML, outerHTML…) |
hx-trigger | trigger = "click" | What triggers the request (click, change, load…) |
In htmxr, these map directly to function parameters, with no JavaScript to write.
Your first htmxr app
The fastest way to see htmxr in action is to run the built-in hello example.
It launches an Old Faithful histogram where a slider controls the number of bins. Let’s walk through how it works.
library(htmxr)
hx_run_example("hello")The page
The page is served by a GET / route. hx_page() wraps the full HTML document and
injects the htmx script automatically. hx_head() handles the <head> tag.
The slider is built with hx_slider_input(). Three parameters connect it to the
server: get is the URL, trigger the event that fires it (debounced 300ms here),
and target the element to replace.
hx_slider_input(
id = "bins",
label = "Number of bins:",
value = 30,
min = 1,
max = 50,
get = "/plot",
trigger = "input changed delay:300ms",
target = "#plot"
)The plot container is a plain <div> with an id. hx_set() adds htmx attributes
to an existing tag, so the plot loads immediately on page load rather than waiting
for the slider to move.
tags$div(id = "plot") |>
hx_set(
get = "/plot",
trigger = "load", # fires once when the element is loaded
target = "#plot",
swap = "innerHTML"
)
The fragment endpoint
The /plot route returns an SVG string, an HTML fragment rather than a full page.
When the slider moves, htmxr sends GET /plot?bins=35. The server returns the SVG. htmx swaps it into #plot. No JavaScript, no JSON parsing, no manual DOM manipulation.
#* @get /plot
#* @query bins:integer(30)
#* @parser none
#* @serializer none
function(query) {
generate_plot(query$bins)
}The htmx connection
slider input event
│
▼
GET /plot?bins=35 ──► server renders SVG
│
◄──────────────┘
htmx swaps SVG into #plot
Anatomy of an htmxr project
A minimal htmxr app needs only two things:
api.R: your plumber2 API with two kinds of routes.
GET /: returns the full page (built withhx_page())GET /fragment: returns HTML fragments (one route per dynamic piece)
hx_serve_assets(): registers the htmx JavaScript file as a static asset on
your plumber2 router.
library(htmxr)
#* @get /
#* @serializer html
function() {
hx_page(
hx_head(title = "My app"),
tags$div(id = "content") |>
hx_set(
get = "/content",
trigger = "load",
target = "#content"
)
)
}
#* @get /content
#* @serializer html
function() {
tags$p("Hello from the server!")
}
Launch with:
plumber2::api("api.R") |>
hx_serve_assets() |>
plumber2::api_run(port = 8080)
ignite() takes no port argument. Pass the port to api() or to api_run(),
otherwise the server silently binds to the default 8080.
Next steps
Explore more built-in examples:
hx_run_example("select-input") # dynamic table filtering
hx_run_example("lazy-load") # lazy loading pattern
When you’re ready to add client-side logic without writing JavaScript, take a look at alpiner, the Alpine.js wrapper for the hyperverse ecosystem.