hyperverse
  • Ecosystem
  • Why
  • Get started

On this page

  • What is hyperverse?
  • Installation
  • How htmx works
  • Your first htmxr app
    • The page
    • The fragment endpoint
    • The htmx connection
  • Anatomy of an htmxr project
  • Next steps

Get started

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:

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 — no JavaScript to write.


Your first htmxr app

The fastest way to see htmxr in action is to run the built-in hello example:

library(htmxr)
hx_run_example("hello")

This launches an Old Faithful histogram where a slider controls the number of bins. Let’s walk through how it works.

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 htmx parameters connect it to the server:

hx_slider_input(
  id = "bins",
  label = "Number of bins:",
  value = 30,
  min = 1,
  max = 50,
  get = "/plot",                         # send GET /plot on trigger
  trigger = "input changed delay:300ms", # trigger: input event, debounced 300ms
  target = "#plot"                       # replace the content of #plot
)

The plot container is a plain <div> with an id. hx_set() adds htmx attributes to it so the plot loads immediately on page load:

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, not a full page:

#* @get /plot
#* @query bins:integer(30)
#* @parser none
#* @serializer none
function(query) {
  generate_plot(query$bins)
}

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.

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:

pr <- plumber2::api("api.R") |>
  hx_serve_assets()

pr$ignite(port = 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.