Transitions
Put transition:fade on an element and it fades in when a
<show> block puts it on the page, and fades out when the
block takes it away. That is the whole feature.
It runs on plain CSS - JAF ships no animation engine.
<script>
let open = false
</script>
<button onclick="open = !open">Toggle</button>
<show when={open}>
<div class="panel" transition:fade>Fades in, and fades out again.</div>
</show>
<style>
button {
font: inherit;
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
button:hover {
border-color: var(--accent);
}
.panel {
margin-top: 0.75rem;
padding: 0.75rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
</style>What JAF does for you
Fading something in is easy without any framework help.
Fading it out is not: by the time you would start the animation, the element has already been removed from the page.
So JAF keeps a leaving element in the document until its CSS transition finishes, and only then detaches it.
It writes the CSS too. Each preset is a pair of classes, one applied while the element enters and one while it leaves, and JAF adds its own stylesheet the first time a transition runs.
For the built-ins you write no CSS at all.
To change how they feel, override four custom properties. Set them wherever
you like - on :root for the whole app, or on one panel:
:root {
--jaf-transition-duration: 180ms;
--jaf-transition-easing: cubic-bezier(0.2, 0, 0, 1);
--jaf-transition-distance: 8px; /* how far slide travels */
--jaf-transition-scale: 0.96; /* where scale starts */
}
<show>, <switch>,
<dynamic> and <for> all qualify. A
{condition && ...} expression or a
.map() list does not: those are patched in place, so there is no
add or remove for JAF to animate and the attribute does nothing.
The three built-ins
transition:fade- fades.transition:slide- fades, with a small vertical shift.transition:scale- fades, with a slight zoom.
They take no value. transition:fade="200ms" is not a thing; use
the custom properties above to change the timing.
<script>
let showFade = true
let showSlide = true
let showScale = true
</script>
<div class="preset-row">
<div class="preset-col">
<button onclick="showFade = !showFade">Toggle</button>
<show when={showFade}>
<div class="preset-card" transition:fade>fade</div>
</show>
</div>
<div class="preset-col">
<button onclick="showSlide = !showSlide">Toggle</button>
<show when={showSlide}>
<div class="preset-card" transition:slide>slide</div>
</show>
</div>
<div class="preset-col">
<button onclick="showScale = !showScale">Toggle</button>
<show when={showScale}>
<div class="preset-card" transition:scale>scale</div>
</show>
</div>
</div>
<style>
button {
font: inherit;
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
button:hover {
border-color: var(--accent);
}
.preset-row {
display: flex;
gap: 0.75rem;
}
.preset-col {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-height: 6rem;
}
.preset-card {
padding: 1rem 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-family: var(--font-mono, monospace);
font-size: 0.875rem;
text-align: center;
}
</style>On a component tag: write the element yourself
transition: on a component tag is a build error.
Declare it on an element the block inserts:
<!-- WRONG: a build error -->
<show when={open}>
<Panel transition:fade />
</show>
<!-- RIGHT: an element of your own, which is what the block inserts -->
<show when={open}>
<div transition:fade><Panel /></div>
</show>
Changed in 0.1. The tag form used to work by decorating the wrapper element JAF rendered around every component. There is no wrapper now, so there is nothing for the declaration to land on and the compiler says so.
Declare it where the block can see it. A preset written inside the child's own template is not the same thing: the block animates what it inserted, and it was not handed a transition for a declaration it cannot see. Two presets that do meet on one element compose - an element already entering is never entered twice.
Your own classes
transition:class="enter leave" swaps in two class names of your
own. Same machinery, same wait before removal, your CSS:
<script>
let open = false
</script>
<button onclick="open = !open">Toggle</button>
<show when={open}>
<div class="tag" transition:class="tag-in tag-out">Your classes, JAF's timing</div>
</show>
<style>
button {
font: inherit;
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
button:hover {
border-color: var(--accent);
}
.tag {
display: inline-block;
margin-top: 0.75rem;
padding: 0.4rem 0.75rem;
border-radius: 999px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
}
.tag-in {
animation: tag-in 240ms ease both;
}
@keyframes tag-in{
from {
opacity: 0;
transform: translateX(-12px);
}
}
.tag-out {
opacity: 0;
transform: translateX(12px);
transition: opacity 240ms ease, transform 240ms ease;
}
</style>
Write the enter class as a @keyframes animation - it starts the
moment the class lands on the new element.
The leave class must be a CSS transition, because
transitionend is the signal JAF waits for before detaching the
element.
Whole-page transitions
A separate switch animates entire route changes, using the browser's own View Transitions API. One line of config covers every navigation:
// config/jaf.config.js
export default {
router: {
transitions: true // or 'fade' | 'slide' | 'zoom'
}
}
Per-route and per-navigation overrides are listed under router.transitions.
Give an element transition:name="hero" on both pages and the
browser morphs it from one position to the other.
Browsers without the API get an instant page swap, and under
prefers-reduced-motion: reduce nothing animates at all.
For anything richer - staggering, spring physics, FLIP moves - reach for a real animation library through a use: action.
animate:, in: and out: are not part of
JAF today.