Skip to content

Repository files navigation

Vibely

Describe a landing page in one sentence. Get back a live, scroll-animated page you can preview, edit section by section, and download.

🌐 vibelyislive.vercel.app

Vibely is an AI agent that plans, writes copy for, and generates a complete animated landing page — GSAP ScrollTrigger choreography, Lenis smooth scroll, the whole thing — from a prompt like:

"a landing page for a monospace font foundry, dark and nerdy"

No templates. Every run produces a different page: different copy, different palette, different combination of scroll effects, all generated live.


The core idea: closed vocabulary, open choreography

Most "AI website generator" demos produce a static page, because scroll-driven animation is exactly where LLM-generated code breaks. Pin math, smooth-scroll integration, and reveal timing are narrow-but-deep failure surfaces — and a page that looks animated but has one section stuck invisible is worse than a page that never tried.

Vibely's answer: the model never writes the fragile parts.

  • A fixed runtime harness owns Lenis, the GSAP ticker, plugin registration and refresh timing — byte-identical on every generated page. Generated code is forbidden from even naming those symbols; the linter rejects the tokens.
  • The model chooses from a closed set of 27 animation intents (plus none). Each one has a param schema, a hard mechanical contract, and a deterministic fallback implementation.
  • Inside those walls the model is fully free. It invents the DOM, the layout, the copy, the palette and the choreography. Two pages using the same intent look nothing alike.

The vocabulary is genuinely closed at the type level. Adding an id to the enum without also shipping a contract card and a fallback skeleton is a startup crash by designassertIntentsHaveCards and assertSkeletonsComplete run at module load, so the vocabulary cannot drift out of sync with what can actually be rendered.

The 27 intents, by category

Category Intents
Entrance fade-up-stagger · split-text-reveal · mask-wipe · scale-settle · count-up-stats · line-mask-reveal · stagger-grid-reveal · clip-shape-morph · blur-focus-reveal · svg-path-draw · text-scramble-cycle
Scroll-linked parallax-drift · reverse-parallax · scrub-choreography · sticky-card-stack · scroll-velocity-skew · motion-path-drift
Pinned horizontal-scroll-track · pinned-step-sequence · reverse-scroll-reveal · pinned-caption-scrub
Ambient marquee-loop · gradient-mesh-drift · counter-rotate-marquee · magnetic-cta · sheen-sweep
Global theme-shift
None none — a deliberately static section

They arrived in three tiers. The first 14 need no GSAP plugin beyond ScrollTrigger and SplitText. Tier A added seven more that still pass the linter unchanged. Tier B/C added six, three of which lean on plugins vendored into the harness (DrawSVG, MotionPath, ScrambleText) and three of which need no plugin at all.


How a page gets built

prompt (+ optional reference image)
  │
  ▼
1. PLANNER      — theme (palette, fonts, style archetype) + section list
                  + one animation intent per section          [1 LLM call, JSON]
  ▼
2. COPYWRITER   — real headline/body/stats/CTA copy per section [1 LLM call, JSON]
  ▼
3. CODE GEN     — per section, IN PARALLEL: HTML + CSS + a GSAP init function
                  each prompt carries only THAT section's contract card
  ▼
4. VALIDATE     — 10 structural rules per section; 1 auto-repair attempt;
                  else fall back to the hand-written skeleton for that intent
  ▼
5. ASSEMBLE     — inject validated sections into the fixed runtime harness
  ▼
6. PREVIEW      — sandboxed iframe · regenerate any single section · export

Code generation is per-section and parallel, not one whole-page call. Three things follow from that: each output stays small enough for the model to get right, sections fail independently, and regenerating one section is possible at all.

Nothing hard-fails. Bad JSON gets one repair call, then deterministic correction. Bad generated code gets one repair call, then falls back to the hand-written skeleton for that exact intent. A run always produces a complete page.

Params are clamped, never rejected. clampParams cannot throw: out-of-range numbers clamp to the legal range, invalid enums fall back to the default, unknown keys are dropped, missing keys are filled. Every correction produces a human-readable warning surfaced in the UI — so a planner that asks for stagger: 4.0 gets a working page and a note, not an error.


The validator

Ten rules, nine of them machine-enforced. This is what makes generated animation reliable rather than probable.

Rule
C1 All DOM access via the injected root. Forbidden: document., window., fetch(, eval(, localStorage, setTimeout, innerHTML =
C2 Animate only transform channels, opacity/autoAlpha, clipPath — plus backgroundColor for theme-shift alone. Never top/left/width/height/margin/fontSize
C3 Eases from a whitelist, including 4 harness-registered signature eases referenced by name only. No bounce, no elastic
C4 Every ScrollTrigger targets root or a descendant. Only pin-category intents may pin, only pin: root, always with anticipatePin: 1
C5 Never reference Lenis, never call ScrollTrigger.refresh/scrollerProxy/config, never gsap.registerPlugin — the harness owns all of it
C6 Exactly one <section data-section> root. All copy verbatim from the CopyDoc — no invented text. No <script>, <style>, or inline on*= handlers
C7 Selectors scoped to the section. No position: fixed; position: sticky only for sticky-card-stack
C8 Size budget: HTML ≤ 160 lines, CSS ≤ 160, JS ≤ 80
C9 All initial hidden/offset states are set in JS, never in CSS
C10 Layout must hold with JS disabled (prompt-only; caught indirectly by C9)

C9 is the load-bearing one. The harness skips init under prefers-reduced-motion, so a section that hid its content in CSS would be invisible forever for those users. One rule, enforced by regex over the CSS, is what makes every generated page degrade to a fully readable static document with JavaScript off — not a special code path, a consequence.


Models: verified, not remembered

Provider config is a pure data table. Groq and OpenRouter are both OpenAI-compatible, so DeepSeek and Cerebras drop in as config rows with no new adapter code. Users can pick a model per run.

npm run verify:models fetches each provider's live /models endpoint and diffs it against the table. Model ids move fast and anything asserted from memory is stale within weeks — a plausible-looking id is not a served one.

The defaults are set by measurement, and the measurements were surprising:

  • Gemini 3 Pro is the strongest on structure and theme reasoning — and is not a default. On a free key it has a hard limit: 0, so every call 429s immediately. It stays as an opt-in pin for a billed key.
  • Gemini 3 Flash was the default until Google reshuffled quotas to 20 requests per day — about two page generations before everything 429s.
  • Gemini 3.1 Flash Lite now leads every stage. Head-to-head on the same 12-concurrent burst: 12×200 in 1.75s, where regular Flash managed 3/12 in 26.6s. Combined with a 250k token/min ceiling, it is the only model in the registry that can actually feed an 8-way codegen fan-out on a free key.

Groq's limits in the table are read off live x-ratelimit-* response headers rather than docs. The section fan-out width is computed from each provider's declared rpm, and a sliding-window rate gate queues around the cap instead of letting a 429 cascade into repair → fallback skeleton.


Try it

npm install
npm run dev

Set a provider key in .env, open the app, type a prompt, hit Generate.

Thirteen verify:* scripts cover the pieces that are easy to break silently:

npm run verify:models     # model ids against live provider catalogues
npm run verify:variety    # do repeat runs actually differ?
npm run verify:cards      # every intent's contract card, swept
npm run verify:origin     # is generated code really generated, not skeleton?
npm run verify:vision     # reference-image input path
npm run verify:export     # exported page runs standalone

verify:origin exists because of a real failure: the contract cards used to embed runnable reference skeletons that were character-identical to the fallback skeletons — so successful generations quietly converged on the failure path's DOM, and the variety the whole design exists to produce disappeared. The cards now keep only the non-obvious ScrollTrigger configuration, and a script checks that outputs don't match the skeletons.

Stack

Layer Choice
Framework TanStack Start (React 19 + TypeScript + Vite 8)
LLM Multi-provider registry — Gemini · Groq · OpenRouter (OpenAI-compatible)
Schema Zod — one shared contract per pipeline stage
Animation (generated pages) GSAP 3.13 — ScrollTrigger, SplitText, Observer, CustomEase, DrawSVG, MotionPath, ScrambleText — plus Lenis
Preview isolation sandboxed <iframe> (opaque origin, allow-scripts only)
Concurrency p-limit fan-out sized from provider rpm + a sliding-window rate gate

Limitations

  • Rate-limited by design. A page is 8–10 small calls across planner, copywriter and per-section codegen. On a free key, back-to-back generations queue rather than run instantly — the rate gate makes that a wait, not a failure.
  • The vocabulary is closed. An effect that isn't one of the 27 intents gets approximated by the nearest one. Adding a genuinely new effect is a real code change — enum id, param spec, contract card, fallback skeleton, category — not a prompt tweak. That cost is the point, but it is a cost.
  • No cross-section aesthetic pass. Sections are generated independently and share only the page theme and their neighbours' kind. Cohesion is good, not guaranteed; there is no "does this page's rhythm work" judgment anywhere.
  • Runtime errors are isolated, not self-healed. A section that throws at runtime is flagged with a regenerate option rather than silently retried.
  • Single export format. One self-contained .html file (or a zipped project). No accounts, no saved history, no multi-page sites.

About

Describe a landing page in one sentence, get back a live scroll-animated page you can download. The model writes the DOM, copy and choreography — never the fragile GSAP/Lenis scroll engine, which a fixed runtime harness owns. 27 animation intents, 10 enforced structural rules.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages