Component Refs
Component Refs
Developer Agent Kit — documentation written for AI agents, readable by humans.
Refs provide access to DOM elements declared as interactive in the contract. They are the second parameter of the interactive constructor.
Single Refs
A ref maps to one DOM element:
# Contract
- tag: addToCart
type: interactive
elementType: HTMLButtonElement
// Component
.withInteractive(function MyComp(props, refs) {
refs.addToCart.onClick(() => {
// handle click
});
})
Ref Methods
Refs provide type-safe access to the DOM element:
refs.submitButton.onClick(() => {
/* ... */
});
// exec$ gives direct access to the element and current ViewState.
// Only use exec$ inside event handlers — never at top-level component
// creation or in effects, because elements don't exist yet at that point.
refs.submitButton.onclick(() => {
refs.submitButton.exec$((element, viewState) => {
element.disabled = viewState.isSubmitting;
});
});
Collection Refs
When an interactive tag is inside a repeated sub-contract, the ref becomes a collection. In jay-html, collection refs use the $ suffix:
<div forEach="items" trackBy="id">
<button ref="itemButton$">Click</button>
</div>
The $ is stripped from the name in the contract and component code:
# Contract
- tag: items
type: sub-contract
repeated: true
trackBy: id
tags:
- tag: id
type: data
dataType: string
- tag: itemButton
type: interactive
elementType: HTMLButtonElement
Collection Ref Methods
// Map over all items in the collection
const labels = refs.itemButton.map((proxy, viewState, coordinate) => {
return viewState.name;
});
// Find a specific item
const target = refs.itemButton.find((viewState) => viewState.id === 'target-id');
// Find by coordinate
const target = refs.itemButton.find((viewState, coordinate) =>
sameCoordinate(coordinate, ['item-2', 'itemButton']),
);
Element Types
Common element types for interactive tags:
| Element Type | Use For |
|---|---|
HTMLButtonElement |
Buttons, clickable |
HTMLAnchorElement |
Links |
HTMLInputElement |
Text inputs, checkbox |
HTMLSelectElement |
Dropdowns |
HTMLTextAreaElement |
Multi-line text |
HTMLFormElement |
Forms |
HTMLDivElement |
Generic containers |
Multiple element types (when the same ref may bind to different elements):
- tag: trigger
type: interactive
elementType: HTMLButtonElement | HTMLAnchorElement
Data + Interactive
A tag can be both data and interactive:
- tag: quantityInput
type: [data, interactive]
dataType: number
elementType: HTMLInputElement
This generates both a ViewState field and a ref.
DOM access rules (Jay Stack pages)
Refs are the only supported path from TypeScript to elements Jay renders. Direct document access bypasses the framework and can break rendering, updates, and performance.
jay-stack validate warns on document.querySelector, document.getElementById, document.createElement, and document.addEventListener in page and component .ts files. Suppress with // jay-dom: allow on the same line when an exception is genuinely needed.
Do
- Declare elements in jay-html with
ref="...". - Drive overlays, lists, and visibility with ViewState (
if,forEach, signal-backed bindings). - Attach handlers with ref APIs:
refs.myRef.onclick,oninput,onkeydown, etc. - Use
refs.myRef.exec$((element, viewState) => …)inside handlers for focus, scroll, measure, or native APIs. - For drags:
setPointerCaptureon the ref element that receivedpointerdown, then listen on that element.
Avoid
document.querySelector/getElementByIdto find template elements — use refsdocument.createElement+appendChildfor UI that belongs in jay-html — useforEachwith ViewStatedocument.addEventListenerfor global events — use the root ref pattern (below)
Root ref pattern — replacing document.addEventListener
Wrap the page content in a shell element with a ref. Use capture-phase listeners on the shell to intercept events before they reach children — functionally equivalent to document.addEventListener.
Setup
<!-- jay-html -->
<div ref="shell" class="page-shell">... entire page content ...</div>
# Contract
- tag: shell
type: interactive
elementType: HTMLDivElement
Global keyboard navigation
Instead of document.addEventListener('keydown', ...):
.withInteractive(function Page(_props, refs) {
let keyboardNav = false;
refs.shell.onkeydown(({ event }) => {
if (event.key === 'Tab') keyboardNav = true;
});
refs.shell.onmousedown(() => {
keyboardNav = false;
});
})
Events bubble up from children to the shell — a handler on the shell sees all keyboard and mouse events from the entire page.
Use refs.shell.addEventListener(type, handler, { capture: true }) only when you need to intercept events before children handle them (e.g., preventing default on specific keys).
Focus management (scroll into view)
Instead of document.addEventListener('focusin', ...):
refs.shell.onfocusin(({ event }) => {
if (!keyboardNav) return;
const el = event.target as HTMLElement;
refs.shell.exec$(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
});
Detecting pointer leaving the page
Instead of document.addEventListener('mouseleave', ...):
refs.shell.onpointerleave(({ event }) => {
// Pointer left the shell — equivalent to leaving the viewport
// if the shell covers the full viewport
});
Ensure the shell has no margin/padding gap so it covers the full viewport. pointer-events: auto (the default) is sufficient.
Finding elements by class → use refs
Instead of document.querySelector('.site-header'):
<!-- jay-html -->
<header ref="siteHeader" class="site-header">...</header>
refs.siteHeader.onclick(() => {
/* ... */
});
refs.siteHeader.exec$((el) => {
el.classList.add('is-hidden');
});
Dynamic lists → use forEach
Instead of creating elements with document.createElement in a loop:
<!-- jay-html -->
<div forEach="cards" trackBy="id" class="card-grid">
<div class="card">
<img src="{imageUrl}" alt="{title}" />
<span>{title}</span>
</div>
</div>
Update the ViewState to add/remove cards — the framework handles DOM creation.
Using exec$ for native DOM APIs
For DOM operations that refs don't wrap (scroll, focus, measurements), use exec$ inside an event handler:
refs.myInput.onclick(() => {
refs.myInput.exec$((el) => {
el.focus();
el.select();
});
});
refs.scrollContainer.exec$((el) => {
el.scrollTo({ top: 0, behavior: 'smooth' });
});
Rare document exceptions
Use only when no ref can exist, with // jay-dom: allow to suppress the validation warning:
| Case | Example |
|---|---|
| Offscreen processing | document.createElement('canvas') // jay-dom: allow for image export |
| Coordinate hit-testing | document.elementFromPoint(...) // jay-dom: allow during drag |
| Tests | document.dispatchEvent // jay-dom: allow in Vitest |
About this document
This page is part of the Jay Stack Agent Kit — documentation generated from the framework source and written primarily for AI agents. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation. Learn more about the Agent Kit →