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.
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:
<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:
<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?
loadrevealedclickevery 0s
Show answer
Answer: load
load fires when htmx processes the element, so the slow part loads after the page has shown.