v1 rewrite: context-first GSAP integration for modern Angular - #1
Merged
Conversation
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>
luishcastroc
force-pushed
the
v1-rewrite
branch
from
August 10, 2026 09:17
bfaffec to
4eadda5
Compare
/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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-managedgsap.context():target()/targets()afterRenderEffect), so@if/@foroutput exists, including on reactive re-runscontextSafe()for event handlers12 template directives on the same engine:
reveal,stagger,splitReveal,scrambleText,counter,parallax,drawSvg,drag,scrollTo,observe,hover, andsequence(composes children into one timeline). Scroll-driven directives take ascrollerinput (selector, element, or ElementRef) to follow any scrollable container instead of the window;observeexposes Observer'spreventDefault.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:
easeReverse, where signal inputs rebuild the timeline reactivelyscrollerEvery multi-file snippet renders in one tabbed container, and every demo renders exactly what its code shows.
Tooling
Inferred Nx targets (
@nx/vite,@nx/eslintplugins) with no deprecated executors, vite 8 nativeresolve.tsconfigPaths, and a prerender pipeline that survives Angular 22's default incremental hydration.Verification