Skip to content

v1 rewrite: context-first GSAP integration for modern Angular - #1

Merged
luishcastroc merged 48 commits into
mainfrom
v1-rewrite
Aug 13, 2026
Merged

v1 rewrite: context-first GSAP integration for modern Angular#1
luishcastroc merged 48 commits into
mainfrom
v1-rewrite

Conversation

@luishcastroc

@luishcastroc luishcastroc commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What this is

A ground-up rewrite of angular-gsap for Angular 22 / Nx 23 / GSAP 3.15, replacing the 2023 directive-based approach.

The API

Core: injectGsap() — vanilla GSAP inside an Angular-managed gsap.context():

  • Host-scoped selectors; signal view queries via target()/targets()
  • Created after render (afterRenderEffect), so @if/@for output exists, including on reactive re-runs
  • Signal-reactive: signals read in the callback revert + re-run it on change
  • SSR-safe, zoneless, no rxjs/zone.js in the bundle
  • Auto-cleanup on destroy; callbacks can return extra cleanup (ticker loops, listeners)
  • contextSafe() for event handlers

12 template directives on the same engine: reveal, stagger, splitReveal, scrambleText, counter, parallax, drawSvg, drag, scrollTo, observe, hover, and sequence (composes children into one timeline). Scroll-driven directives take a scroller input (selector, element, or ElementRef) to follow any scrollable container instead of the window; observe exposes Observer's preventDefault.

The library imports no GSAP plugins; apps register what they use via provideGsap({ plugins, config, defaults, effects }), which also works in lazy route providers for per-chunk splitting.

Docs

Bilingual (en/es) docs site with light/dark themes (the accent palette dims in dark mode) and a live demo plus full source for every feature:

  • signal-driven tweens, timelines, ScrollTrigger, SplitText, Flip, Draggable with inertia
  • an Observer momentum loop, continuous sections (ported from GreenSock's Observer demo), and a radial menu built on GSAP 3.15's easeReverse, where signal inputs rebuild the timeline reactively
  • SVG drawing/morphing/motion paths and a WebGL cube
  • a directives page with a single switchable card: pick a directive and the live demo and its complete component source (tabbed html/ts/css) swap together; scroll demos run inside their own frames via scroller
  • an API reference with plugin coverage and tree-shaking guidance

Every multi-file snippet renders in one tabbed container, and every demo renders exactly what its code shows.

Tooling

Inferred Nx targets (@nx/vite, @nx/eslint plugins) with no deprecated executors, vite 8 native resolve.tsconfigPaths, and a prerender pipeline that survives Angular 22's default incremental hydration.

Verification

  • 40 unit tests across context lifecycle, reactivity, scoping, SSR, directives, plugin detection, and container scrollers
  • lint + build green in CI; docs prerendered (en + es) for GitHub Pages

luishcastroc and others added 27 commits August 9, 2026 12:25
Replaces the 2023 Angular 17 workspace with a fresh Nx 23 monorepo:
- libs/core: publishable @angular-gsap/core (ng-packagr, vitest)
- apps/demo: examples app (esbuild, vitest)
- pnpm 11 with build-script allowlist and supply-chain release-age policy
The Angular equivalent of @gsap/react's useGSAP(): vanilla GSAP code runs
in a gsap.context() scoped to the component host, created via
afterRenderEffect (so template DOM is up to date), outside change
detection, skipped on the server, signal-reactive (revert + re-run on
change), and auto-reverted on destroy. contextSafe() wraps event handlers
so their animations join the context cleanup.

provideGsap() optionally registers plugins and applies global
config/defaults (browser only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tText

Five routed pages that demo the library with visible source: a SplitText
hero that animates the logotype, signal-driven ring choreography,
timeline transport controls via contextSafe, a scroll-scrubbed timeline,
and reactive text splitting. Design concept: static chrome is ink on
porcelain; saturated color is reserved for animated elements.
setup-node@v5 resolves the pnpm cache from the packageManager field and
fails if the pnpm binary isn't installed yet.
4200 tends to collide with other Angular dev servers.
Template-level, preset-based entrance directives built on the injectGsap
engine — typed signal inputs (preset, on, delay, duration, distance,
ease, start), ScrollTrigger-aware via on="scroll" (with a dev warning
when the plugin isn't registered), prefers-reduced-motion respected,
reverted on destroy. Deliberately not raw-TweenVars wrappers; everything
beyond a preset entrance stays in injectGsap.
Per API review: intention-first selectors without the gsap prefix
(standalone directives are opt-in per component, so prefixes add
ceremony without preventing real collisions). provideGsap/injectGsap
keep their names — there GSAP is the intention, matching the
provideX/injectX idiom. GsapRevealPreset becomes RevealPreset.
Replaces apps/demo with apps/docs built on Analog: file-based routes,
SSR + full prerender (a live proof of the library's SSR safety), and a
CodeSnippet component that renders source through Shiki (lazy,
code-split) — Analog's runtime ContentRenderer is a no-op by design, so
inline snippets call Shiki directly. Adds a Directives example page,
port 4300 default, and GH Pages deploy from the prerendered output with
DOCS_BASE-driven base href.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
toggleActions 'play none none reset' lets a reveal self-correct when its
ScrollTrigger is created already past the start position (tall viewports,
SPA navigation while scrolled): scrolling back above the start rewinds it
and it replays on entry. Adds a spec covering ScrollTrigger creation and
the toggleActions contract.
…ison

- /flip: FLIP-animated signal-driven filter (capture before the signal
  changes the DOM, animate after Angular renders)
- every example page gets a 'what the library is doing here' section
- home replaces the single snippet with a side-by-side comparison of
  plain GSAP in a component vs injectGsap, plus the reasons that matter
- scroll restoration on navigation; scroll demo gets a viewport of runway
- GSAP logo favicons (referential use; official mark stays Webflow's)
- prose pass across docs and README: no em dashes, plainer sentences
Unwrap viewChild/viewChildren signal queries (or plain ElementRefs) into
the DOM elements GSAP expects. Passing the query signal inside an
injectGsap callback keeps it tracked, so viewChildren picking up new
elements re-runs the animation with the DOM already rendered.
- examples now show the template and the component side by side as
  labeled panels, so the cleanliness of the approach is visible
- basics and the hero snippet use viewChild/viewChildren with the new
  target()/targets() helpers instead of selector strings
- home drops the vanilla-GSAP comparison column for a straightforward
  presentation, and adds tree-shaking and runtime-cost feature cards
- new /pointer example: gsap.quickTo cursor follower through contextSafe
- README: Targeting elements section, Small and fast section, no more
  strawman code block
Park the chaser in the stage center with an idle pulse so the demo
reads as active before any pointer input, and drop mix-blend-mode
(invisible under dark-mode browser extensions that repaint the page).
- /reference: the full exported surface (injectGsap options and GsapRef,
  provideGsap, target/targets, both directives' inputs, type re-exports)
  as tables with usage snippets. Not /api, which Analog reserves for
  server routes.
- example explanations rewritten in a casual, factual voice under a
  'How this works' heading, dropping the bold mini-lecture labels
Three more preset directives on the injectGsap engine, bringing the
template surface to five:
- splitReveal: SplitText entrance by chars/words/lines, per-kind stagger
  defaults, original markup restored on destroy
- counter: counts the element's text to a number, Intl-formatted, final
  value shown immediately under reduced motion
- parallax: scroll-scrubbed vertical drift as the element crosses the
  viewport; negative speeds move against the scroll

Plugin detection never imports GSAP plugins (they'd land in every
bundle): ScrollTrigger is found via gsap globals where it registers
itself, SplitText structurally among the provideGsap plugins via DI,
minification-safe.
- directives page demos all five (split line, locale-formatted counters,
  parallax blocks in the scroll section) with GSAP_DIRECTIVES in the
  ts panel
- page titles renamed to plain feature names (Signal-driven animations,
  Timelines, Text splitting, FLIP layout transitions, …)
- reference tables and README cover the full directive set
- injectGsap callbacks can return a cleanup function, forwarded to
  gsap.context(): the way to tie gsap.ticker callbacks and listeners to
  the component lifecycle (used by the WebGL example)
- sequence directive: provides a timeline via DI; reveal, stagger,
  splitReveal, and drawSvg inside it play in template order (gap on the
  container, GSAP position syntax via [at] on children) instead of
  hand-tuned delays
- drawSvg directive: DrawSVG stroke entrance for a shape or every
  stroked descendant of a container, staggered

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- design pass: cream paper, thick ink borders, hard offset shadows,
  chip eyebrows and snippet tabs, pressable buttons, GSAP-driven
  marquee tape, GSAP logo in the header, octocat on the GitHub button
- /svg: DrawSVG via the drawSvg directive, MorphSVG blob-to-star,
  MotionPath rider; DrawSVG/MorphSVG/MotionPath registered in
  provideGsap (all free since GSAP 3.13)
- /webgl: fragment-shader uniforms tweened like any object, render
  loop on gsap.ticker, torn down by a returned cleanup
- directives board now composes through sequence instead of delays
- /es/* routes re-export each page with a route-provided LOCALE token;
  page copy lives in per-page en/es objects and both trees prerender,
  so Spanish pages are real static URLs. Language pill swaps to the
  same page in the other locale. The API reference stays in English.
- light/dark themes as token swaps (ink and paper trade places, the
  motion palette stays loud); system preference by default, header
  toggle persisted to localStorage with a pre-paint init script; code
  blocks keep a fixed dark background either way
- theme toggling goes through Angular's DOCUMENT token, the header
  logo is bigger, and the eyebrow chips no longer lose their color to
  the .page-head paragraph rule (the invisible-label bug)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SVG: an ink figure-eight draws in, three arrows chase around it with
  MotionPath autoRotate at staggered start offsets, and the shape cycles
  blob to star to bolt (MorphSVG morphs the path and the fill together)
  with an idle bob
- WebGL: anti-aliased dual-hue bands (smoothstep edges, second hue ring)
  whose center chases the pointer through two quickTo setters feeding a
  u_mouse uniform
- theme toggle now does a circular reveal from the button via the View
  Transitions API (WAAPI clip-path); instant fallback for reduced
  motion or unsupported browsers
- /drag: Draggable + InertiaPlugin bricks with momentum, grid snap, and
  a contextSafe scatter; instances are created in the callback so the
  context kills them on destroy
- WebGL example rebuilt as a 3D cube (hand-rolled matrix, flat palette
  faces, ink edges): endless linear spin tween, quickTo pointer tilt,
  elastic Pulse
- /basics renamed to /start; Home link in the nav; install command gets
  pnpm/npm/yarn tabs with click-to-copy, aligned with the hero button
- hero logotype hands color back to the CSS token after the intro
  (clearProps), so switching themes mid-session keeps it visible
- reference tables get explicit thead/tbody, fixing the NG0500
  hydration mismatch
- docs builds use their own Vite cache dir (DOCS_VITE_CACHE) so they
  no longer deadlock against a running dev server
- 'all free' marketing removed from page copy
Angular 21+ fixtures auto-detect by default and signal writes schedule
change detection on their own, so the manual detectChanges calls did
nothing. Tests now also run with zoneless: true, matching how the
library is meant to be used.
…fects

Closing the remaining directive-shaped gaps from the GSAP toolset:
- drag: declarative Draggable with bounds ('parent' default), grid
  snap, inertia, and dragStart/dragEnd outputs; instances killed on
  destroy (Draggable identified by its static create + hitTest, since
  its instance methods are constructor-assigned)
- scrambleText: text decodes into place (ScrambleTextPlugin), sequence
  and on="scroll" aware, plain text under reduced motion
- scrollTo: smooth-scroll to a selector on click via ScrollToPlugin,
  native scrollIntoView fallback without it
- observe: GSAP Observer gestures as Angular outputs (up/down/left/
  right/press/release), instance killed on destroy
- provideGsap({ effects }) registers gsap.registerEffect entries
- property plugins are found by their literal name strings in the
  provideGsap array, minifier-safe

Docs: drag page dogfoods the drag directive (raw API shown alongside),
the sequence board gains a scrambleText line, reference and README
cover the full 11-directive set, and the reference explains why
ScrollSmoother stays un-wrapped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each card now names a situation an Angular developer recognizes:
selector scoping framed like style encapsulation, cleanup framed as
nothing-to-write-in-ngOnDestroy, SSR framed as 'ng add @angular/ssr
without window-is-not-defined', tree-shaking framed as bundle budgets,
and runtime cost framed as change detection staying idle. Spanish
matches.
Pointer micro-interactions as a preset directive (lift, grow, shrink,
tilt): enter and leave tweens overwrite each other so fast passes never
pile up, and reduced motion disables it. The docs header now runs on
it: nav links lift, the pills and GitHub button grow, the logo tilts.
Grouped sidebar (Learn / Core / Plugins / Beyond the DOM / Reference)
replaces the top nav row. The open/close is the library's own motion:
width tween on desktop, off-canvas slide with backdrop fade on mobile,
staggered link entrance, and a hamburger that morphs to an X.

Accessibility and responsiveness:
- skip-to-content link, aria-expanded/aria-controls on the toggle,
  ariaCurrentWhenActive on links, localized labels
- mobile drawer moves focus to the first link, closes on Escape or
  backdrop click and returns focus to the toggle, closes on navigation
- reduced motion swaps every tween for instant class-driven state
- one GSAP gotcha fixed on the way: the CSS translateX(-104%) initial
  state parses as an x component, so GSAP zeroes x when taking over
  or the two offsets stack

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/loop combines the three plugins that lacked a showcase: chips ride a
closed path through paused motionPath tweens used as position setters,
one Observer stream merges wheel/touch/pointer input, and InertiaPlugin
glides the loop's progress (a plain number) with the real release
velocity. The reference gains a catalog-wide coverage table (including
eases, dev tools, and canvas bridges) and a tree-shaking section with
the lazy-route provideGsap pattern.
Catalog-wide table (through the eases, dev tools, and canvas bridges)
plus the lazy-route provideGsap pattern for per-chunk plugin splitting.
Twelve self-contained cards, one per directive, each with the full
component.ts and component.html as they would sit in a real project
(plus a stylesheet where the example needs one: drag and parallax).
Plugin requirements are noted as comments in the TypeScript. Localized
descriptions in both trees.
Each complete-example card now pairs a running demo of the directive
with a single tabbed code container (ts / html / css) instead of
stacked panels. Tabs are proper role=tablist/tab/tabpanel with arrow
keys; entrance demos get a Replay button, the drag demo is draggable,
observe counts wheel/swipe, and scrollTo jumps between cards.
A framing paragraph in the style of @gsap/react's positioning: GSAP
needs no wrapper to run in Angular; this library handles only the
Angular-specific friction points. The benefits grid gains an honesty
line (it's the glue you'd otherwise write yourself), a stronger scroll
entrance, and each card now lifts through the hover directive instead
of CSS.
All remaining selector-string usage in the examples moves to signal
view queries with target()/targets(): the hero and marquee, the
timeline bars, the scroll-scrubbed shapes, the SplitText passage, and
the Flip cards. CSS classes stay documented as the scoped alternative.
Also rewrites the benefits note in plain words and sweeps stray em
dashes from recent copy.
/sections ports GreenSock's Animated Continuous Sections Observer demo
(credited and linked) to signal view queries and injectGsap: layered
wrapper reveals, parallax backgrounds, SplitText headings scattering in
per character, wrap-around navigation, and an animating guard, all torn
down with the component.

Copy pass: the benefits section leads with 'Your GSAP animations, the
Angular way' and plain card titles (No cleanup needed, SSR ready, Zero
change detection cost); design-meeting voice removed from user-facing
docs; the README examples section describes what readers get instead of
how the docs app is built.
Port of the GSAP radial FAB demo: items spring out along an arc with
elastic.out and close with their own ease via easeReverse (GSAP 3.15).
The controls are signals read inside the injectGsap callback, so the
timeline rebuilds itself when they change.
Inline styles from one mode survived a resize into the other, leaving
the panel translated off-screen while still reserving space. Crossing
the breakpoint now re-normalizes the panel, backdrop, and hamburger,
and the desktop open/closed preference sticks until the user changes it.
GSAP's Observer accepts preventDefault but the directive never exposed
it, so wheel gestures over an observed element also scrolled the page.
Off by default, matching Observer.
The live demos had drifted from the code beside them: reveal showed one
scale-in card instead of the three documented entrances, scrambleText
was missing the second target-text element, and stagger, splitReveal,
counter, parallax, drawSvg, sequence, drag, observe, and hover all
showed different content or options than the snippets. Each demo now
renders what the code says, and the observe deck uses preventDefault
so wheeling it does not scroll the page.
Every on-scroll directive (reveal, stagger, splitReveal, scrambleText,
counter, drawSvg) and parallax now accepts a scroller: a selector, an
element, or an ElementRef pointing at the scrollable container that
drives the trigger. Defaults to the window, matching ScrollTrigger.
The explicit executor targets shadowed the @nx/vite and @nx/eslint
plugins already configured in nx.json, and both executors are
deprecated for Nx 24. Dropping them lets vite/vitest/eslint run
directly. The vite configs use vite 8's resolve.tsconfigPaths instead
of the deprecated nxViteTsPaths plugin, pin analog's workspaceRoot so
prerender output stays in dist/apps/docs, disable vitest watch for the
inferred test target, and carry the dev-server port.
Angular 22's provideClientHydration() enables incremental hydration by
default, which brings event replay along; its whenStable callback races
prerender teardown and throws 'window is not defined' on every route.
The docs have no @defer hydrate blocks, so withNoIncrementalHydration()
loses nothing.
Every page that stacked an html panel over a ts panel now shows a
single CodeTabs container, html tab first.
The page had three demo areas doing versions of the same thing: the
hero sequence board, twelve stacked example cards, and an on-scroll
section that needed a full page of runway. They are now one card with
a directive picker that swaps the live demo and the code together.
The reveal and parallax demos scroll inside their own frames via the
new scroller input, the example code shows exactly what each demo
renders, and selectors and file names match the tab labels.
The four accents kept their light-mode neon values on a dark
background. Dark mode now gets muted variants via the theme tokens,
and every hardcoded hex goes through them: style bindings use var(),
GSAP color tweens read the token's computed value, and the hero
logotype's tick takes a class instead of an inline color so theme
switches keep working.
The workspace root became an inferred project whose build script is
'nx run-many -t build', so running that target recursed forever and
hung CI.
vite build completes but never exits: sass-embedded's compiler keeps
the event loop alive (vitejs/vite#18127). A post-ordered buildApp hook
runs after analog's whole ssr/prerender pipeline and exits, guarded on
the freshly prerendered output existing so it can never fire from one
of the nested builds.
@luishcastroc
luishcastroc merged commit 5f1b84a into main Aug 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant