Directives Reference
Every JAF directive, at a glance. Each links to its full page.
class:
Toggle a class from an expression.
<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}>
style:
Bind one CSS property, or one custom property.
<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'}>
bind:
Two-way binding for form controls.
<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.
on:
Events with modifiers.
Without modifiers, plain onclick="..." is shorter and does the
same job.
<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
preventDefault | event.preventDefault() |
stopPropagation | event.stopPropagation() |
once | Handler runs only once |
self | Only if the event started on this element |
capture | Capture-phase listener |
passive | Passive listener |
Key modifiers
escape, enter, tab, space | Those keys |
up, down, left, right | Arrow keys |
delete, backspace | Those keys |
ctrl, alt, shift, meta | Combinable 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.
ref:
Capture a DOM element into a variable.
<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.
use:
Run a function on an element when it is created.
<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>.
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.
<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:fade | Opacity |
transition:slide | Fade plus a small vertical offset |
transition:scale | Fade 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:
<img transition:name="hero" src="/photo.jpg" />
Route transitions run on the View Transitions API. Turn them on in config:
// 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.
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.