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>
output

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().

source
<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:

source
<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:row inside a <for>. One variable cannot hold N elements. Use use:, 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 to onMount() 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>
output

For behaviour you want on more than one element, and that cleans up after itself, use an action instead - see use:.