Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Controls when effects activate. Separates effect behavior from activation timing.
The data-trigger attribute controls when an effect activates. It separates the visual effect from its activation moment, allowing the same effect to be triggered by scroll, hover, click, or a timed delay.
If no data-trigger is set, CSS-only effects activate immediately and JS effects run on load.
Activates when the element scrolls into the viewport (10% visible threshold). Uses IntersectionObserver and fires once.
<h1 data-effect="fade-in slide-up" data-trigger="scroll">Appears on scroll</h1><p data-effect="reveal" data-trigger="scroll">Words reveal when visible</p>
Activates on pointer enter, deactivates on pointer leave. For CSS-only effects, the :hover pseudo-class drives the animation directly.
<span data-effect="wiggle" data-trigger="hover">Hover to wiggle</span><h2 data-effect="glitch" data-trigger="hover">Hover for glitch</h2>
Toggles the effect on click. Adds [data-effect-active] on first click, removes it on second.
<button data-effect="shake" data-trigger="click">Click to shake</button><h2 data-effect="pop" data-trigger="click">Click to pop</h2>
Activates after a delay of n milliseconds. Uses setTimeout.
<h1 data-effect="fade-in" data-trigger="time:2000">Appears after 2 seconds</h1><p data-effect="slide-up" data-trigger="time:500">Half-second delay</p>
Drives the effect's animation continuously from the element's view progress through the scrollport, via CSS animation-timeline: view(). Unlike scroll (which fires once via IntersectionObserver), view-progress ties the animation directly to scroll position — stop scrolling mid-reveal and the animation pauses at that exact frame.
<article data-effect="fade-in" data-trigger="view-progress"> Opacity follows scroll position</article>
The range controls which slice of the element's scrollport journey drives the animation. Default is entry 0% cover 50% — animation reaches its end keyframe by the time the element is halfway covered.
| Variant | animation-range | When the animation plays |
|---|---|---|
view-progress | entry 0% cover 50% | Default. Starts as element enters viewport, finishes when half-covered. |
view-progress:entry | entry 0% entry 100% | Only while entering the viewport (bottom edge crossing). |
view-progress:exit | exit 0% exit 100% | Only while leaving the viewport (top edge crossing). Pair with exit-slot effects (fade-out, collapse). |
view-progress:cover | cover 0% cover 100% | Across the entire time the element is in view. |
view-progress:contain | contain 0% contain 100% | Only while element is fully visible (smaller than the scrollport). |
<h1 data-effect="slide-up" data-trigger="view-progress:entry"> Slides up only during entry</h1> <p data-effect="fade-in" data-trigger="view-progress:cover"> Fades across the full time in view</p>
data-trigger="scroll-progress" ties the animation to the root scroller's total progress (0% at top, 100% at bottom of the page) rather than the element's view progress. Useful for whole-page indicators and parallax decoration.
<div data-effect="float" data-trigger="scroll-progress"> Drift bound to page scroll</div>
view() supported, scroll() more limited).layout.css.scroll-driven-animations.enabled.The rules are wrapped in @supports (animation-timeline: view()). Browsers without support see the element's idle (hidden) state — no broken layout. For a one-shot fallback, add scroll as a peer trigger: data-trigger="scroll view-progress" — the existing IntersectionObserver trigger fires in older browsers; newer browsers run the timeline-driven version (CSS specificity favors the timeline rule).
prefers-reduced-motion: reduce (and the project-wide :root[data-motion-reduced] opt-in) suppress the timeline binding entirely and snap the element to its final state — no scroll-linked motion at all.
Generalised IntersectionObserver trigger. intersect and intersect:once fire once on first viewport entry (same behaviour as scroll). intersect:toggle re-activates on every viewport entry and deactivates on every exit — useful for decoration effects that should follow the viewport.
<h2 data-effect="fade-in slide-up" data-trigger="intersect">Fires once</h2><aside data-effect="neon" data-trigger="intersect:toggle">Glows only while on screen</aside>
Activates while a matchMedia query matches, deactivates on mismatch. Reacts to media-query change events so a decoration switches on at a breakpoint, in dark mode, or when reduced-motion lifts.
<h1 data-effect="neon" data-trigger="media:(prefers-color-scheme:dark)"> Neon only in dark mode</h1><p data-effect="float" data-trigger="media:(min-width:60rem)"> Floats only on wide screens</p>
Note: the query must not contain whitespace — data-trigger is split on whitespace into multiple triggers.
Listens for a custom DOM event by name on the element. Pairs with VB.activate(el) so app code can drive effect timing without touching data-effect-active directly.
<h1 id="title" data-effect="pop" data-trigger="event:order-confirmed"> Thanks for your order!</h1><script> document.getElementById('title').dispatchEvent( new CustomEvent('order-confirmed') );</script>
Defers activation until VB.swap()'s view transition commits the new DOM. Bridges the data-transition system into the effect lifecycle, so an entrance animation runs in sync with the cross-fade instead of before it.
<section data-transition="effect:fade"> <h2 data-effect="pop" data-trigger="vt"> Pops in after the cross-fade </h2></section>
When data-trigger is omitted, CSS-only effects activate immediately via their CSS selectors. JS effects run their activate() function on initialization.
<h1 data-effect="shimmer">Shimmers immediately</h1><h2 data-effect="neon" class="pink">Glows immediately</h2>
An effect doesn't know what triggered it. The same fade-in slide-up effect behaves identically whether activated by scroll, hover, click, or a timer.
Register custom triggers via the VB.trigger() API. Custom triggers work identically to built-in ones.
VB.trigger('visible', (el, activate, param) => { const threshold = parseInt(param || '50', 10) / 100; const io = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { io.disconnect(); activate(); } }, { threshold }); io.observe(el); return () => io.disconnect();});
<h2 data-effect="fade-in" data-trigger="visible:50">50% visibility</h2>
| Trigger | Implementation | Notes |
|---|---|---|
scroll | IntersectionObserver | Fires once at 10% visible, then disconnects. Alias of intersect:once. |
hover | CSS :hover + JS events | CSS-only effects use :hover. JS effects use pointer events. |
click | addEventListener('click') | Toggles [data-effect-active]. |
time:n | setTimeout | n is delay in milliseconds. |
intersect:once|toggle | IntersectionObserver | once (default) fires on first entry; toggle activates/deactivates on every enter/leave. |
view-progress[:entry|:exit|:cover|:contain] | animation-timeline: view() | Continuous, scroll-linked. Drives the animation from the element's view progress through the scrollport. |
scroll-progress | animation-timeline: scroll() | Continuous, bound to root-scroller progress (0% top, 100% bottom). |
media:(query) | matchMedia | Activates while query matches; deactivates on mismatch. No whitespace in the query. |
event:name | addEventListener | Activates when the named DOM event fires on the element. |
vt | vb:vt-update-done | Fires after VB.swap()'s view transition commits. Pairs with data-transition="effect:class". |