A personalized digital greeting card: pick an occasion, write a message, add an optional hand-drawn doodle, and reveal it from an animated envelope with a matching confetti burst. Cards auto-save locally and restore on reload, and a "Copy Share Link" button encodes the whole card into a URL — open it anywhere and see that exact card, no account or backend required.
What makes it work
- Four occasions, four real themes. Birthday, Holiday, Thank You, and Congratulations each carry their own accent color and confetti shapes — balloons, snowflakes, hearts, stars — a genuinely different visual moment per occasion, not one template with a recolored button.
- An explicit share always beats a quiet background save. If a visitor opens a shared card on a device that already has its own saved card, the shared link wins every time — it's an intentional action and shouldn't be silently overridden. A corrupted share link shows a friendly error; a corrupted local save just fails quietly, since only one of those is something the user actually asked for.
Design at a glance
- ink · #2b2b33
- surface · #ffffff
- birthday · #ff6f91
- holiday · #2f9e6f
- thank you · #d69e2e
- congrats · #6b5bd6
System font stack throughout — the color shift per occasion carries the personality, not the typography.
- Envelope reveal — pending capture
Built to last
- Native browser APIs only — Canvas, Web Audio,
localStorage,URLSearchParams— zero dependencies, zero build step - Envelope-reveal animation and confetti both respect
prefers-reduced-motion - Doodle/signature deliberately excluded from shared links — it only ever exists on the device that drew it
For developers — the full technical breakdown
01. A shared link always wins over a local save
- Problem
- If a visitor opens a shared card link on a device that already has its own saved card in
localStorage, it's ambiguous which one should display. - Choice
- The shared link always takes priority — it's an explicit action by the visitor and shouldn't be silently overridden by a background auto-save they may not even know is happening.
- Result
- Predictable behavior in the one scenario most likely to confuse someone: opening a card that isn't theirs.
02. Persistence lives in share.js, not card.js
- Problem
- Both the shareable-link feature and
localStoragepersistence solve the same underlying problem — turning a card into a portable string and back. - Choice
- Reused
encodeCardData()/decodeCardData()from one module (share.js) instead of duplicating that logic across two files. - Result
card.jsstays focused purely on rendering and validating card content, not storage mechanics — and if persistence later needs more complexity, there's already a clean seam to split it out.
Accessibility & performance
:focus-visiblestates andaria-livestatus regions throughoutprefers-reduced-motionskips or shortens the envelope animation and confetti- Invalid or corrupted share links show a friendly error and fall back to the blank composer, never a blank or broken page
- Zero dependencies, zero build step — every effect (confetti, doodle canvas, chime) is a native browser API
What I’d do differently
The doodle/signature is deliberately left out of shared links — a raster image would make the URL impractically long. A v2 with a small hosted backend could store the doodle server-side behind a short share ID instead, without changing the local-only fallback for people who never share.
Stack
- HTML5 / CSS3
- Vanilla JavaScript
- Canvas API
- Web Audio API
- URLSearchParams
- localStorage