Directives Reference

Every JAF directive, at a glance. Each links to its full page.

class:

Toggle a class from an expression.

source
<div class:active={isActive}>

<!-- Shorthand: uses the variable of the same name -->
<div class:active>

<!-- Alongside a static class, and as many as you like -->
<div class="btn" class:primary={isPrimary} class:disabled={isDisabled}>

Full documentation →

style:

Bind one CSS property, or one custom property.

source
<div style:color={textColor}>
<div style:background-color={bg}>

<!-- CSS custom property -->
<div style:--primary={brandColor}>

<!-- Units are yours to add -->
<div style:width={width + 'px'}>

Full documentation →

bind:

Two-way binding for form controls.

source
<input bind:value={name} />
<textarea bind:value={bio}></textarea>
<input type="checkbox" bind:checked={agreed} />
<details bind:open={expanded}>

<!-- A number, or null while the box is empty -->
<input type="number" bind:value={count} />

<!-- A single select binds the chosen value -->
<select bind:value={country}>

<!-- A multiple select binds an ARRAY: initialise it [] -->
<select multiple bind:value={countries}>

<!-- A radio group binds ONE variable, the chosen radio's value -->
<input type="radio" name="size" value="s" bind:value={size} />
<input type="radio" name="size" value="m" bind:value={size} />

<!-- Shorthand: uses the variable of the same name -->
<input bind:value />
Directive Elements it binds
bind:value <input>, <textarea>, <select>
bind:checked <input type="checkbox">, <input type="radio">
bind:open <details>, <dialog>
bind:<prop> on a component tag Syncs that prop name between the two components' state, both ways, with the raw value intact

On any other standard element the directive is dropped with a [jaf] warning naming the tag - a <div> has no value and never fires input, so both halves would be dead.

Custom elements and an <input> whose type is an expression are left alone.

Full documentation →

on:

Events with modifiers.

Without modifiers, plain onclick="..." is shorter and does the same job.

source
<button on:click={handleClick}>

<form on:submit|preventDefault={handleSubmit}>
<a on:click|preventDefault|stopPropagation={handle}>

<input on:keydown|enter={submit} />
<input on:keydown|escape={close} />

<button on:click|once={handleOnce}>
<div on:click|self={handleSelf}>

Modifiers

preventDefaultevent.preventDefault()
stopPropagationevent.stopPropagation()
onceHandler runs only once
selfOnly if the event started on this element
captureCapture-phase listener
passivePassive listener

Key modifiers

escape, enter, tab, spaceThose keys
up, down, left, rightArrow keys
delete, backspaceThose keys
ctrl, alt, shift, metaCombinable with any of the above

There are no letter keys, and no esc alias. An unknown modifier is dropped silently, so on:keydown|ctrl|s={save} fires on Ctrl plus any key and on:keydown|esc={close} fires on every keydown. For a letter shortcut, take ctrl and check event.key yourself.

Every modifier works wherever a handler compiles - the static template, a <for> row, a <show>, <switch> or <guard> arm, a <teleport> body, an overlay body. capture, passive and once are real addEventListener options in all of them, and in a row once is per row.

A handler on a component tag compiles in either spelling (<Button onclick="go()">, <Button on:click|once={go}>): the body joins the writing component's handler table and the child attaches it to its own root element, so it is never a prop. It needs the child to have a single root element, and is refused when the child declares that name as a prop or nothing forwards (noprops, no-forward).

One position still differs: inside another component's slot content the on: directive is dropped with a warning, where a plain onclick="..." still compiles.

Full documentation →

ref:

Capture a DOM element into a variable.

source
<script>
let inputRef

onMount(() => inputRef.focus())
</script>

<input ref:inputRef />

Refs are assigned after the DOM exists, which is after the script body has run - read them in onMount() or in an event handler, never at the top of the script.

On a component tag it is a build error: a component is inserted at a comment anchor, so there is no element for the name to hold. Wrap the tag in an element of your own and put the ref there - and note that element is still empty during your onMount, because the child mounts on a later turn.

A repeated or deferred body is refused for the same kind of reason: ref: in a <for> row is one name for N elements, and in a <show> arm, a <teleport> body or an overlay body the element does not exist until that body mounts. Use use: instead.

Two elements claiming one name is a compile warning, and the last one wins.

Full documentation →

use:

Run a function on an element when it is created.

source
<script>
function autofocus(el) { el.focus() }

function clickOutside(el, handler) {
  const onClick = (e) => { if (!el.contains(e.target)) handler() }
  document.addEventListener('click', onClick)
  return () => document.removeEventListener('click', onClick)
}
</script>

<input use:autofocus />
<div use:clickOutside={closeMenu}>
<span use:tooltip="Help text">

The first argument is always the element.

Return a function, or { destroy() }, for cleanup; return { update(), destroy() } to be told when a reactive parameter changes.

A parameter naming a loop variable only works in <for>. In a .map() row the action still runs, on the right element, but the parameter arrives as undefined, because a use: parameter is evaluated later against component state and only a <for> row rescopes it. The compiler names this. Pass something the component itself holds, read the row's data off the element, or use a <for>.

Full documentation →

transition:

Three element presets, plus your own classes. They animate content a control-flow block adds or removes - <show>, <switch>, <dynamic>, <for> - and do nothing on a plain element or on .map() / {cond && ...} output.

source
<show when={open}>
  <div transition:fade>Fades in and out</div>
</show>

<!-- On a component tag it is a build error: write the element yourself -->
<show when={open}>
  <div transition:scale><Panel /></div>
</show>

<!-- Your own classes: first while entering, the rest while leaving -->
<div transition:class="my-enter my-leave">
transition:fadeOpacity
transition:slideFade plus a small vertical offset
transition:scaleFade plus a slight zoom
transition:class="..."Your classes. The leave must be a CSS transition - JAF waits for transitionend

These are valueless attributes; transition:fade="200ms" is not a thing.

Retune them with --jaf-transition-duration, -easing, -distance and -scale.

Under prefers-reduced-motion: reduce nothing is applied.

transition:name

The one reserved key under the prefix, and it belongs to route transitions rather than the presets.

Same name on both pages, and the browser morphs the element between them:

source
<img transition:name="hero" src="/photo.jpg" />

Route transitions run on the View Transitions API. Turn them on in config:

source
// config/jaf.config.js
router: { transitions: true }              // 'fade', 200ms

router: { transitions: 'slide' }           // 'fade' | 'slide' | 'zoom' | 'none'

router: {                                  // per route, most specific wins
  transitions: { default: 'fade', routes: { '/admin/*': false } }
}

To override one navigation, import navigate from jafjs/router and pass { transition: 'slide' }, or { transition: false } to skip it once.

Full documentation →

There is no animate:, in: or out:. They are specified but not implemented on any path, and writing one is a build error rather than an attribute that silently does nothing. For FLIP reordering, springs or staggering, point a use: action at an animation library.