Shway
A landing page that makes the pitch by letting you do it. Shway is “Tinder for your plans”, so the hero is a deck of plan cards you can drag, flick and match with — the product's core gesture, playable before the download.
The problem
An app-store landing page has one job, getting the download, and one problem: the product lives on a phone, and a visitor on a laptop has never touched it. Screenshots describe the swipe; they do not let you feel it.
The rebuild also had to be a drop-in replacement for the page before it: the same contact endpoint and fields, the same /event-details/ share links that open the app, analytics on the store buttons — while getting faster, more accessible and properly marked up for search.
How it works
No framework and no build step: one HTML document, one stylesheet, one main.js, and GSAP's plugins loaded as vendor scripts. Every effect is progressive enhancement — each init function checks for GSAP and for prefers-reduced-motion first, so the page is complete and usable before, or without, any animation.
Lenis handles smooth scrolling and is driven from GSAP's own ticker, with ScrollTrigger.update on every Lenis scroll event — one clock for both, so scroll-linked animation never drifts against the scroll position.
index.html · site.css · main.js no build step
│
│ vendor: gsap · ScrollTrigger · Draggable · SplitText
│ lenis · canvas-confetti
▼
main.js — one IIFE, one init per section
guard: hasGsap? prefers-reduced-motion?
│
├─ initDeck Draggable swipe deck · velocity flick
│ JOIN / NOPE stamps · "You Matched!" reveal
├─ introHero SplitText word reveal after fonts.ready
├─ initHow IntersectionObserver drives the phone screen
├─ initGallery autoplay belt with named pause "holds"
├─ initStats ScrollTrigger count-up, once
├─ initStories CSS animationend is the autoplay timer
├─ initNavSpy scroll-spy + sliding nav indicator
├─ initVideo Vimeo iframe loads only while open
└─ initContact validate, then POST to the existing backend
│
▼
Lenis smooth scroll ── driven by gsap.ticker ── ScrollTrigger.update
/event-details/:slug ──► shway:// deep link ──► store fallback after 2s
Decisions worth defending
- 01
The hero is the product's gesture, not a picture of it.
The deck is six plan cards on GSAP
Draggable. Release velocity is smoothed on every drag frame (0.8 × current + 0.2 × previous), so a short, fast flick decides a card just as a long drag past 110px does. Drag right and a JOIN stamp fades in, left and NOPE does; a miss snaps back on an elastic ease. A match opens a “You Matched!” card through aclip-pathcircle reveal with confetti — and fires an analytics event, so the toy is also measured. - 02
Motion is an enhancement, never a dependency.
Every animated init checks
hasGsapandprefers-reduced-motionbefore doing anything. With reduced motion the deck still works — buttons and arrow keys decide cards instantly — the gallery becomes a plain snap carousel with no autoplay, the testimonials stop rotating, and the typed text appears whole. Nothing is hidden behind an animation that might not run. - 03
Autoplay that knows when to stop.
The gallery belt scrolls itself and pauses for named reasons — hover, touch, keyboard focus, mouse drag, an arrow nudge, the tab being hidden, the belt being off-screen. Each reason is held and released independently, so one ending never restarts the belt while another still applies. Keyboard focus only counts when it is
:focus-visible, because a tap also focuses the track and would otherwise freeze it. - 04
One clock for scroll and animation.
Lenis is advanced from
gsap.tickerwith lag smoothing off, and every Lenis scroll event callsScrollTrigger.update. Two independent animation loops would let scroll-triggered motion run a frame behind the scroll; one shared ticker keeps them in step. The video modal stops Lenis while it is open and restarts it on close. - 05
A redesign that breaks nothing it replaced.
The contact form posts the same fields to the same backend endpoint as before, and
/event-details/<slug>share links still try theshway://deep link first, falling back to the App Store or Play Store after two seconds. A redesign that quietly breaks inbound links and lead capture is a regression, however good it looks. - 06
Heavy assets load on demand.
The Vimeo iframe has no
srcuntil the modal opens and is blanked again on close. Gallery and section images are lazy-loaded; only the first deck card's image is preloaded, because it is the largest thing above the fold.
Proving it works
The page is live, and everything here can be checked in its source.
- Structured data — a JSON-LD graph with
Organization,WebSite,MobileApplicationand an eight-questionFAQPagethat matches the eight FAQ entries on the page. - Crawl surface —
robots.txt,sitemap.xml, a canonical URL and full Open Graph tags. - Accessible interaction — the deck is a labelled, focusable region that answers to arrow keys; only the top card is exposed to assistive tech; a polite live region announces skips and matches; Escape closes the match dialog.
- Form validation without a library — required fields are checked on submit and marked
aria-invalid, focus moves to the first problem, and the status line says what to fix. - Deployed on Vercel behind HSTS.