What's Included
index.html— the whole site: nav, invitation-plate hero, the photograph, The Day (timeline), Travel & Stay, Questions & gifts, RSVP, footerassets/style.css— all styles, responsive, print of the palette rationale at the topassets/script.js— all behaviour, documentedassets/script.min.js— the same, minified for productionassets/img/couple.webp— the photo print, an optimised black-and-white placeholder you replace with your own photo
No build step. Deploy the folder as static files anywhere.
Quick Start
- Open
index.htmland change the obvious things: the names (hero, nav monogram, footer, the reply card'sM & Eheaders, the envelope address), the date, the venue, and the reply-by date. Search forMartha,Elliot,Foxhill,12 Septemberand31 July— those five searches find every instance. - Replace
assets/img/couple.webpwith a photo of you two, and rewrite the print's caption inindex.html— see The photograph below. - Rewrite the copy in your own voice: the timeline of your day, your travel notes and hotels, your Q&A. Delete whole sections freely — the script skips anything it can't find.
- Point the form somewhere real — see Wiring the form below. Until you do, submitting plays the animation but records nothing.
- Swap
script.jsforscript.min.jsbefore you go live:
Code snippet omitted: it ships with the download.
The signature: the reply card
The form is the real, accessible interface. The card beside it is decoration (aria-hidden) that mirrors the form live:
- Each form control carries
data-card-input="key", and the card has a matching[data-card-field="key"]line. Text inputs update after a 350ms pause in typing; radios update on change. Only the changed line is retyped, character by character, with a blinking caret while it runs. - The accept/decline radios carry
data-card-key="accepts"/data-card-key="declines"instead, and put a typedXin the matching box on the card. - Menu radios can carry
data-card-textfor what the card should type (e.g. the value posted to your endpoint isBaked river trout, while the card typesbaked river trout). - Declining folds the rest away. The supper, dietary and song questions live in a
[data-attend-extras]wrapper; picking "regretfully declines" folds the wrapper closed, disables its inputs (so a decline never posts a main course) and wipes their card lines. Picking "joyfully accepts" brings it all back, answers intact. Any field you add inside the wrapper gets this behaviour for free; without JavaScript the wrapper simply stays visible, and nothing in it is required.
Adding a field: add the input with data-card-input="mykey", add a card line containing <span data-card-field="mykey"></span>, done. Removing a field: delete both halves; nothing else references them.
Changing the menu
The three mains live in the Supper — choose your main fieldset. Change the value (what gets submitted), the visible <span> (what guests read), and data-card-text (what the card types). Add or remove choices freely — the card only ever shows the one that is picked.
Posting the envelope
On submit, one GSAP timeline scrolls the section into view, shrinks the card, raises the envelope to meet it, drops the card inside, closes the flap, thumps the stamp on, rolls the postmark over it, and posts the envelope off-page. Then a plain confirmation appears where the card was. The confirmation copy lives on the form as data-done-message, and the failure copy as data-error-message — edit both in the HTML, not in the script.
The envelope's address block, the stamp monogram and the postmark text are all in index.html inside [data-envelope] — change them along with the names. The postmark's date is yours to set too (guests will see it for under a second, but it's the details that get screenshotted).
Wiring the form
The form is a plain <form method="post">, so it works with any form backend, with or without JavaScript.
Formspree — create a form at formspree.io, then:
Code snippet omitted: it ships with the download.
That's the whole job. With JavaScript on, the script sees a real action, POSTs the reply with fetch first, and only plays the envelope once the submission has actually succeeded; a failure shows the written error and re-enables the button instead. With JavaScript off, the form submits normally to Formspree's hosted thank-you page.
Netlify Forms — if you deploy on Netlify, add the attributes and a form name, and keep action="#"... but note that Netlify intercepts the native POST, so with JavaScript on you should give the form its page path as action (e.g. action="/") so the fetch posts somewhere Netlify records:
Code snippet omitted: it ships with the download.
Mailto (last resort) — action="mailto:you@example.com" method="post" enctype="text/plain" opens the guest's mail client with the answers in the body. It's clunky and depends on a configured mail app; use a form service if you can. With a mailto: action the script skips the fetch and just plays the send after the mail window opens.
No backend yet — leave action="#". Submitting plays the animation and shows the confirmation, but nothing is recorded; don't send the link to guests like this.
Sections
Every section is independent: delete any of them (or the nav, or the footer) and the rest keeps working.
- Hero — the invitation plate. Names, date, venue, reply-by chip, button to
#rsvp, and the photo print riding the plate's top corner (in the flow at the plate's foot on phones). The plate's double rule is two borders in.plate/.plate::before; the print is a<figure>inside the plate — swap the image and caption (see The photograph below), or delete the figure to keep the plate all type. - The Day (
#day) — the timeline. Each<li class="tl-row">is a time (typewriter mono), a dot and a body; add or remove rows freely. The spine draws down as it is read (scrubbed ScrollTrigger). - Travel & Stay (
#travel) — getting there and the hotel place cards. The barn line-art is inline SVG; replace or delete it. - Questions (
#questions) — a<dl>of Q&A pairs plus the gifts note. - RSVP (
#rsvp) — the form and the card. On desktop the card sits sticky beside the form; on phones it rides compact under the nav while the form is filled, so every answer is seen landing on it.
The photograph
The one photo on the page is a placeholder for your own. assets/img/couple.webp is a bundled, optimised black-and-white WebP derivative of an Unsplash photo (free to use commercially), not the original: the grade is baked into the pixels so the print sits with every palette, including any you mix yourself.
To use your own photo: export it around 1000×1250 (a 4:5 portrait), ideally black and white to keep the print palette-proof, save it over assets/img/couple.webp (any WebP/JPEG exporter works — keep the filename, or update the <img> src), then rewrite the caption and the alt text in index.html. The print's white mount, tilt, shadow and corner position are CSS on .photo-print (with its phone placement in the 760px media block); a different aspect ratio just needs the width/height attributes on the <img> updated to match.
Original photo: Vadim Paripa on Unsplash. From a GSAP Vault checkout the derivative regenerates with bun scripts/build-template-assets.ts wedding-template --force.
Palettes
Four ready palettes ship in assets/style.css. The default is champagne — a champagne lining, near-white card stock, espresso ink and one antique gold. Three alternates live in the ALTERNATE PALETTES block at the end of the file:
- powder — powder-blue lining, midnight ink, a wax-seal bordeaux
- blush — dusty-rose lining, warm near-black ink, the same bordeaux
- sage — eucalyptus lining, forest ink, a deep botanical green
To use one, set the attribute on the <html> tag:
Code snippet omitted: it ships with the download.
Or, once you've settled, copy that block's values into :root and delete the alternates block entirely — the tidier end state. Every pair of colours in all four palettes is measured to WCAG AA composited as used; each block notes its worst pair.
To mix your own instead: the four colours are declared once at the top of assets/style.css with matching RGB triples. Every translucent value on the page derives from the triples, so a reskin is editing four hexes and four triples — there are no stray rgba() literals to hunt.
Code snippet omitted: it ships with the download.
Three derived values to keep in step: --ground-deep (the alternate section ground, a slightly deeper lining), --accent-deep (button hover) and --card-shade (the envelope flap). The header comment above :root records every measured contrast pair; if you change a colour, re-measure the pairs listed there — especially the dimmed ink (--dim) on --ground-deep, the accent on --ground-deep, and the placeholder colour. The accent fails on ink in every shipped palette (~2:1), which is why the footer carries no accent; keep that rule if you keep a dark footer.
Smooth scrolling
Lenis is wired in and fully optional — delete its <script> tag and everything still works (the envelope scroll-into-view falls back to native smooth scrolling). The instance is exposed as window.lenis so your own code can drive the same scroll rather than fight it.
Accessibility
- The reply card and envelope are
aria-hiddendecoration; the form is a standard labelled form with native validation (requiredon name and attendance). - The confirmation is an
aria-live="polite"region and receives focus when it appears, so the fold never hides the outcome from a screen reader or strands the keyboard; the form column is madeinertduring the animation. - Reduced motion (both the CSS media query and the GSAP matchMedia branch): no reveals, no type-on — card lines update instantly — and submitting swaps straight to the confirmation with no envelope.
- Without JavaScript the page renders complete and static, and the form submits natively to its
action. - All type meets WCAG AA on every surface it appears on, measured composited; the table is in the stylesheet header.
Browser Support
Modern browsers (ES5 syntax, but clip-path, inset and :focus-visible assume evergreen). Not compatible with IE11.
Dependencies
- GSAP 3.12+ (core)
- ScrollTrigger plugin
- Lenis (optional — smooth scrolling; the page works without it)
Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.