This assumes scroll-reveal is installed, including its src/styles/scroll-reveal-custom.css.
Add an effect
Write one rule in scroll-reveal-custom.css, describing the hidden state. The element transitions from that rule to how it normally looks.
Put it inside the @media block the file already has. That block keeps the effect off when JavaScript is off or reduced motion is on, as scroll-reveal.css does for its own effects. Outside it, a page without JavaScript never leaves the hidden state.
@media (scripting: enabled) and (prefers-reduced-motion: no-preference) { [data-reveal="tilt"]:not([data-reveal-state="in"], [data-reveal-state="off"]) { rotate: -4deg; }}Then use the name like any built-in effect:
<div data-reveal="tilt">A card that tilts into place</div>Every reveal already starts at opacity 0, so the rule only needs what makes the effect its own.
Directions
Add a direction to the name: tilt-up, tilt-left, tilt-down-right. There is no extra rule to write. The direction rules in scroll-reveal.css match the end of the name, so they combine with yours. The eight directions are listed on the snippet page.
Animating other properties
Where you can, use opacity, translate, scale, rotate, filter and clip-path. Those transition by default, and they leave any transform the element already has alone.
Some effects need something else. A skew has no property of its own, so it needs transform. In that case set --reveal-properties to the full list, or the effect jumps into place. Keep translate in the list if the effect should work with directions.
@media (scripting: enabled) and (prefers-reduced-motion: no-preference) { [data-reveal^="skew"] { --reveal-properties: opacity, transform, translate; }
[data-reveal^="skew"]:not([data-reveal-state="in"], [data-reveal-state="off"]) { transform: skewY(6deg); }}Match with ^= so both rules also cover skew-up, skew-left and the rest.
Split a custom effect
The CSS rule does nothing to split pieces, because GSAP animates those. To use data-reveal-split with your effect, register its GSAP start values as well:
import { registerRevealBase } from "./scroll-reveal";
registerRevealBase("tilt", () => ({ opacity: 0, rotate: -4 }));Import that file on any page that uses the effect. It registers tilt and its eight directions for the split path.
If the effect should follow the reveal’s custom properties, read them from the function’s argument: distance, blur, scale, zoomIn and zoomOut, already resolved.
registerRevealBase("sink", ({ distance }) => ({ opacity: 0, y: distance, scale: 0.96 }));An effect registered with registerRevealEffect can also set its own default split, order (such as "random") and stagger, as the built-in scatter and flicker do.
Gotcha
Register at the top level of the file. A registration inside a click handler, a timeout or after
an await can arrive after GSAP has loaded, and the element falls back to fade-up with a
console warning.
React to a reveal
Listen for scroll-reveal:in. It fires on the element and bubbles, so one listener on document hears every reveal:
document.addEventListener("scroll-reveal:in", (event) => { console.log(event.detail.element);});To wait for one element, give it a data-reveal-id and listen for the named event:
<section data-reveal="fade-up" data-reveal-id="stats">...</section>document.addEventListener("scroll-reveal:in:stats", startCounter, { once: true });Leave out { once: true } to run it on every reveal. scroll-reveal:out and scroll-reveal:out:<id> fire on the reset.
Sharing an effect across projects
An effect in scroll-reveal-custom.css belongs to one project. To give it to every project, move it into the snippet itself, following src/content/snippets/scroll-reveal/PROMOTE.md in the Blocks repo.