Ref Directive
ref:name puts the real DOM element into a variable, so you can
call methods on it: focus an input, draw on a canvas, measure a box.
You usually do not need this. Use bind:value
for form values, class: and style: for
appearance, <show> for visibility and
{expression} for text. A ref is for the things those
cannot express.
Why refs exist
Some browser features have no declarative form.
There is no attribute that means focus(), play(),
getContext('2d'), getBoundingClientRect() or
scrollIntoView() - each is a method you call on an element, so
you need the element itself.
document.getElementById() is not a substitute in a single-page
app.
The router mounts a page into a detached wrapper and moves it into the live document afterwards, so a query at mount time finds nothing, and the same component can appear on a page many times, so no selector means my element.
A ref is scoped to your component instance: two components can both
call theirs inputRef and each gets its own node.
Basic usage
Declare the variable with let and attach it with
ref:variableName.
No =, no value - the element is assigned to the variable of that
name.
<script>
let inputRef
function focusInput() {
inputRef.focus()
}
</script>
<div class="ref-row">
<input ref:inputRef type="text" placeholder="Click the button" />
<button onclick="focusInput()">Focus input</button>
</div>
<style>
.ref-row {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.ref-row input {
padding: 0.4rem 0.6rem;
border: 1px solid var(--border-hover);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
}
.ref-row input:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.ref-row button {
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
cursor: pointer;
}
.ref-row button:hover {
border-color: var(--border-hover);
}
</style>When refs are available
A ref is null while the <script> body runs -
the element does not exist yet. It is assigned when the component's DOM is
created, so it is live inside onMount() and in any event handler.
Anything that must run against the element at startup belongs in
onMount().
<script>
let inputRef = null
// Not here. The script body runs before the element exists,
// so inputRef is still null and this would throw:
// inputRef.focus()
onMount(() => {
inputRef.focus() // assigned by now
})
function selectAll() {
inputRef.select() // handlers run after mount, so the ref is live here too
}
</script>
<input ref:inputRef type="text" />
<button onclick="selectAll()">Select all</button>
On a component tag: a build error
<Chart ref:chartRef /> stops the build, naming the tag.
A component is inserted at a comment anchor, so there is no element for the
name to hold: no wrapper, no claim on the child's own root element, and no
component instance - JAF hands none out. The compiler says so rather than
leaving the variable null.
Write the element you want a handle on:
<div ref:chartRef><Chart data={points} /></div>
That <div> is yours, and it is still empty during
your onMount() - a child component mounts on a later turn than its
parent - so it is the right handle for placing, observing or scrolling to the
component, and the wrong one for measuring what the child rendered.
Measure that inside the child, or with a ResizeObserver.
Changed in 0.1. The alpha wrapped every component in a
<div data-jaf-component="Chart"> and handed that to the ref. The
wrapper is gone, so the shape that used to reach it is refused instead.
Where a ref is refused
A ref names one element that exists when your component's DOM does. Three positions
cannot promise that, and each is a build error naming the body rather than a variable
that stays null:
-
A repeated body -
ref:rowinside a<for>. One variable cannot hold N elements. Useuse:, which runs once per row on that row's element. -
A body that mounts later - a
<show>or<match>arm, a<for>fallback, a<teleport>body, an overlay body. When that element exists relative toonMount()is what the compiler declines to promise. Put the ref in the component's own template, or split the body into a component that holds its own. -
Content projected into another component's slot. That markup is mounted
by the component you passed it to, so the ref is dropped with a
[jaf]warning naming it.
Two elements, one name
A ref name holds one element.
If two elements in the same template claim the same name, the last one in document order wins and the compiler warns, naming the ref and quoting both tags - the first element is unreachable from your script. Rename one of them.
Two refs on one element are a different thing and are fine:
<div ref:box ref:target> gives both variables that element.
Canvas
A canvas is the clearest case: its contents have no markup at all, only a drawing context you ask the element for.
<script>
let canvasRef
function draw() {
const ctx = canvasRef.getContext('2d')
ctx.fillStyle = 'royalblue'
ctx.fillRect(10, 10, 100, 100)
}
</script>
<div class="canvas-demo">
<canvas ref:canvasRef width="400" height="160"></canvas>
<button onclick="draw()">Draw</button>
</div>
<style>
.canvas-demo {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.canvas-demo canvas {
max-width: 100%;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
}
.canvas-demo button {
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
cursor: pointer;
}
.canvas-demo button:hover {
border-color: var(--border-hover);
}
</style>
For behaviour you want on more than one element, and that cleans up after
itself, use an action instead - see
use:.