htmx Tutorial · Common Patterns · Lesson 21 of 30

Tabs and Lazy Loading

Build tabs that load their content from the server, and load slow sections after the page appears.

Create a free account to save this page, take notes and track your progress. Sign in

Tabs

Each tab button loads its panel with hx-get. The server returns the whole tab list plus the panel, with the right tab marked aria-selected="true", so the selected state is always correct without JavaScript:

Server-driven tabsTry it yourself
<style>[role=tab][aria-selected=true] { font-weight: bold; border-bottom: 2px solid #0ea5e9; }</style>
<div id="tabs" hx-target="#tabs" hx-swap="outerHTML">
  <div role="tablist">
    <button role="tab" aria-selected="true" hx-get="/tabs/1">Overview</button>
    <button role="tab" aria-selected="false" hx-get="/tabs/2">Pricing</button>
    <button role="tab" aria-selected="false" hx-get="/tabs/3">FAQ</button>
  </div>
  <div role="tabpanel"><p>htmx gives HTML superpowers.</p></div>
</div>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  const panels = { 1: ["Overview", "htmx gives HTML superpowers."], 2: ["Pricing", "Free and open source."],
                   3: ["FAQ", "Does it need a build step? No."] };
  server.get("/tabs/:n", (req) => {
    const n = Number(req.params.n);
    const buttons = Object.entries(panels).map(([i, [title]]) =>
      `<button role="tab" aria-selected="${Number(i) === n}" hx-get="/tabs/${i}">${title}</button>`).join("");
    return `<div id="tabs" hx-target="#tabs" hx-swap="outerHTML"><div role="tablist">${buttons}</div>
            <div role="tabpanel"><p>${panels[n][1]}</p></div></div>`;
  });
</script>

Lazy loading

A slow part of a page (a chart, recommendations, a report) shouldn't delay everything else. Render a placeholder that loads itself with hx-trigger="load"; the rest of the page appears immediately:

Lazy-load a slow sectionTry it yourself
<h3>Dashboard</h3>
<p>This part of the page appears immediately.</p>
<div hx-get="/chart" hx-trigger="load">
  <p>Loading the chart (this takes a while)...</p>
</div>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  server.get("/chart", () => `<p>Visitors this week:</p>
    <div style="display:flex;gap:4px;align-items:end;height:60px">${[20, 35, 30, 50, 60, 45, 70]
      .map((h) => `<div style="width:18px;height:${h}px;background:#0ea5e9"></div>`).join("")}</div>`, { delay: 1500 });
</script>

Test yourself

Which trigger loads a placeholder's content right after it appears on the page?

  1. load
  2. revealed
  3. click
  4. every 0s
Show answer

Answer: load

load fires when htmx processes the element, so the slow part loads after the page has shown.

Was this page helpful?