Open DevTools. Type, click, change inputs. Verify state updates without server round-trips.
:attr="expr" — boolean attributes
:disabled="q('#lock').checked"
:hidden="!q('#lock').checked"
:text — computed text content
Subtotal: $
With 10% tax: $
:html — innerHTML binding
Note: :html is XSS-unsafe if value comes from untrusted source. Prefer :text.
:class — string and object forms
Use :class only when CSS pseudo-classes can't express the rule (cross-element
comparison, arithmetic). For checkbox/focus/invalid state, use CSS (:has(),
peer-checked:). Example below: branch on value-vs-budget comparison — CSS can't see it.
:class="q('#cost').valueAsNumber > q('#budget').valueAsNumber
? 'hidden faded'
: 'visible'"
$
:class="{ big: price > 1000, expensive: price > 100 }"
:style — string and object forms:style="'--pct: ' + q('#p').valueAsNumber / 100"
:aria-* — always "true"/"false" (never removed)Hidden until expanded.
matches() — bare alias for this.matches():disabled="matches(':has(input:invalid)')"
:checked and :value — property + attribute sync
Mirror: (DOM attribute AND .checked property stay in sync)
Source text:
Uppercased mirror:
take()hx-on:click="take('aria-selected', '[role=tab]')"
:hidden="q('#tab-id').matches('[aria-selected=false]')"
toggle() — binary flip + N-value cycling(class.active + aria-pressed)
(cycles through light → dark → auto)
<output> as computed variable
Subtotal: $
Total: $
<output :value="..."> acts as a named computed value. output.value is an alias for
textContent, so the result is readable via q('#subtotal').value elsewhere.
Add your own display:none (or a global rule) if you want it invisible.
Use :text on <output> when you want it visible.
<output id="subtotal" style="display:none" :value="..."></output>
q('#subtotal').value // read elsewhere
.focused via :class + :focus-within:class="{ focused: matches(':focus-within') }"
hx-live="
let term = q('#q').value;
if (!term) { this.textContent = '(empty)'; return; }
await debounce(300);
this.textContent = 'searching for \"' + term + '\"…';
"
hx-live="
let f = q('#filter').value.toLowerCase();
for (let li of q('ul li'))
li.hidden = f && !li.textContent.toLowerCase().includes(f);
"
attr() scope helper — unified attribute access
q() directional selectors
htmx.live.refresh() for non-DOM state
localStorage.setItem(...) htmx.live.refresh() // expressions reading localStorage recompute
data proxy (Alpine x-data style)
State lives on a container as data-* attribute. Descendants inherit reads and
writes via the data scope helper (Proxy). data.foo resolves to the
nearest ancestor with data-foo. Writes target the same ancestor.
Count (read at top level):
Nested child reads ancestor:
Deeper still:
<div data-count="0"> <button hx-on:click="data.count++">+</button> <span :text="data.count"></span> <!-- reactive --> </div>
with (data) { ... } — multi-write handlers
JavaScript with statement scopes property lookups to data, letting
you mutate multiple keys without prefixing. Use sparingly — works only in non-strict mode
(hx-on / hx-live expressions, not modules).
❤️ · 🔁 · 👁
<div data-likes="0" data-shares="0">
<button hx-on:click="with (data) { likes++; shares++ }">Boost</button>
</div>
:class for branching UI
Tab state via cascading data-tab instead of aria-selected. One state attribute,
descendants read it. No take() needed since the state lives in one place.
<div data-tab="home">
<button hx-on:click="data.tab = 'home'" :class="{ active: data.tab === 'home' }">Home</button>
<div :hidden="data.tab !== 'home'">...</div>
</div>