Skip to content

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:

  1. User triggers an event: a click, an input change, a page load, a form submission…
  2. htmx sends an HTTP request to your server (GET or POST)
  3. Your server returns an HTML fragment: a snippet of HTML, not JSON
  4. htmx swaps the fragment into the targeted DOM element

You control this cycle through five core attributes:

Attributehtmxr parameterWhat it does
hx-getget = "/url"Send GET request on trigger
hx-postpost = "/url"Send POST request on trigger
hx-targettarget = "#id"CSS selector of the element to update
hx-swapswap = "innerHTML"How to insert the response (innerHTML, outerHTML…)
hx-triggertrigger = "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.

R console
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.

api.R
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.

api.R
#* @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 with hx_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)
Watch the port

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.