About this template
A four-page portfolio for a wedding and portrait photographer: a cinematic home with the photographer's statement and featured weddings, a gallery index of four full weddings, one story page per wedding told in a film-like sequence of frames with captions, and an info page with packages, testimonials and an inquiry flow that checks the date against a booked list. The default content follows an invented photographer working the Hudson Valley for a decade, but the structure suits any photographer or two-person studio: change the name, region, start year and yearly wedding count and every page still reads true. The home hero is one full-bleed frame under near-black chrome with the photographer's mark in wide-caps film serif; wedding cards are letterboxed with place and season; story pages mix full-bleed and half-bleed frames with quiet captions; the lightbox runs full-screen with keyboard navigation and slow crossfades. Packages spell out hours, delivered frames, second shooter, travel and album; the inquiry form checks a chosen date against an invented booked list and ends in a client-side confirmation — no backend. Italiana sets the film-serif display, Manrope the quiet body, both self-hosted. Photography is detail-driven — venues, backs of heads, dresses on windows, tablescapes, empty dance floors — with no faces anywhere. To adapt it, swap the variables and rewrite the copy in one content file; weddings, packages, booked dates and testimonials all read from it.
The template prompt
The live demo was built from exactly this text — nothing added.
# Wedding Photographer Portfolio
Build this website exactly as described below. Replace the [VARIABLES] with your own values; keep everything else.
## Brief
A four-page portfolio for [BRAND], a wedding and portrait photographer working [REGION] since [FOUNDED], taking [YEARLY] by design. It must read like a printed film portfolio: a near-black cinematic home with the photographer's statement, a gallery index of four full weddings, one story page per wedding (opened with a query, first wedding when opened without), and an info page with three packages, testimonials and an inquiry flow whose date check answers against an invented booked list. Pages: index.html, gallery.html, story.html, info.html. A full-screen lightbox with keyboard navigation runs across the story pages. Everything runs in the browser; nothing is submitted anywhere.
- Audience: Engaged couples 26–38 scanning portfolios at midnight on a laptop, plus mothers-of-the-bride forwarded the link. They have seen fifty grid templates; they want to feel the work is scarce (a handful of weddings), the numbers are honest (hours, frames, prices) and the photographer answers email like a human.
- Goal: Book the inquiry call. Within ten seconds a visitor should feel the gallery is cinematic and the photographer is selective; within two minutes they should reach the packages and check their date.
- Tone: Quiet, specific and a little wry, like a photographer editing at 1am. Short declarative lines, concrete numbers, no SEO mush, no 'capturing your special day'.
## Stack
- Framework: Vite 5 + React 18 + TypeScript
- Styling: Tailwind CSS 3.4
- Libraries: react (MIT), react-dom (MIT), motion (motion.dev, import from 'motion/react') (MIT), lucide-react (ISC), tailwindcss (MIT), postcss (MIT), autoprefixer (MIT), vite (MIT), @vitejs/plugin-react (MIT), typescript (Apache-2.0), @fontsource/italiana (OFL-1.1), @fontsource-variable/manrope (OFL-1.1)
- Build: `npm run build` → static files in `dist/`. No backend, no server code, no external APIs.
- Setup:
- Scaffold with npm create vite@latest using the react-ts template. Four real HTML pages at the project root: index.html (entry src/main-home.tsx), gallery.html (src/main-gallery.tsx), story.html (src/main-story.tsx) and info.html (src/main-info.tsx).
- vite.config.ts: base './', plugins [react()], build { outDir 'dist', sourcemap false, assetsInlineLimit 0, rollupOptions { input { index: resolve(__dirname, 'index.html'), gallery: resolve(__dirname, 'gallery.html'), story: resolve(__dirname, 'story.html'), info: resolve(__dirname, 'info.html') } } }. Do not copy src/ into dist/.
- Tailwind 3.4 through PostCSS (tailwindcss + postcss + autoprefixer), content ['./index.html', './gallery.html', './story.html', './info.html', './src/**/*.{ts,tsx}']. In tailwind.config.ts add every color under Design to theme.extend.colors with kebab-case keys (ink-black, charcoal, graphite, bone, milk, smoke, brass, brass-deep, fern) and fontFamily display ['Italiana', 'Georgia', 'serif'], sans ['Manrope Variable', 'system-ui', 'sans-serif']. Set borderRadius DEFAULT to 4px — film corners are nearly square — and add maxWidth { container: '1240px' }.
- In src/fonts.ts import '@fontsource/italiana/400.css' and '@fontsource-variable/manrope'; import src/fonts.ts and src/index.css from all four entry files.
- File layout: src/content.ts (every string, number, wedding, frame, caption, package, testimonial, booked date, FAQ answer and image path, typed), src/lib/brand.ts (brandSlug = [BRAND] lowercased with every run of non-alphanumeric characters replaced by '-', emailDomain = brandSlug without hyphens + '.com', mailto = 'hello@' + emailDomain), src/lib/storage.ts (safe localStorage get/set wrapped in try/catch, keys prefixed with brandSlug + ':'), src/lib/wedding.ts (reads the story query 'story.html?w=<slug>' from location.search, returns the wedding whose slug matches, or the first wedding — never a not-found).
- State: src/store/inquiry.ts is a tiny external store (useSyncExternalStore) holding the inquiry draft { names, email, date, venue, note } and the submitted flag, persisted to localStorage key brandSlug + ':inquiry'. The lightbox is local component state lifted into src/store/lightbox.ts (shared across the story page) with { open, weddingSlug, index }.
- Components in src/components/: Header.tsx, Footer.tsx, Reveal.tsx (Motion whileInView wrapper), Eyebrow.tsx (small-caps with thin rules), Lightbox.tsx, WeddingCard.tsx, Frame.tsx (an image with letterbox chrome and caption), Marquee.tsx; home: Hero.tsx, Statement.tsx, Featured.tsx, KindWords.tsx, Approach.tsx; gallery: GalleryIndex.tsx; story: StoryHero.tsx, FrameSequence.tsx, StoryMeta.tsx, NextStory.tsx; info: Packages.tsx (TierCard.tsx), Testimonials.tsx, Inquiry.tsx (DateCheck.tsx, BriefForm.tsx, SentCard.tsx), Faq.tsx. src/pages/Home.tsx, Gallery.tsx, Story.tsx, Info.tsx compose them; every page renders Header and Footer.
- Before the first build, download every image listed under Assets into public/images/ with curl -L -o using the exact file names given. The build itself never fetches anything.
- public/favicon.svg as described under Assets. All four HTML files: lang en, <meta name='theme-color' content='#141311'>, <link rel='icon' href='./favicon.svg' type='image/svg+xml'>. Titles: index.html '<title>[BRAND] — Wedding photography in [REGION]</title>', gallery.html '<title>Full weddings — [BRAND]</title>', story.html '<title>Stories — [BRAND]</title>', info.html '<title>Packages & inquiry — [BRAND]</title>'. story.html opened with no query renders the first wedding, titled for it.
- Write ASSETS.md at the project root as described under Assets.
- dist/ holds the four HTML files, hashed JS/CSS, the woff2 fonts, favicon.svg and the six images. Keep it under 12 MB.
## Variables
- [BRAND] — Photographer name (default: "Nora Vale"). The photographer's name as it appears on the site ('Nora Vale', wordmark 'NORA VALE'). Used in the wordmark, titles, email (hello@ + brandSlug without hyphens + .com) and localStorage keys. Always an invented person; never a real photographer or studio.
- [REGION] — Region (default: "the Hudson Valley"). Where the photographer works, lowercase article style ('the Hudson Valley', 'the Pacific Northwest'). Drives the hero line, story place names, travel copy and FAQ. Real regions are fine; venues and towns in the stories stay invented.
- [FOUNDED] — First year shooting (default: "2014"). Four-digit year of the first paid wedding. Drives the statement line, the footer 'since [FOUNDED]' and the About eyebrow.
- [YEARLY] — Weddings per year (default: "18 weddings a year"). The scarcity line ('18 weddings a year, deliberately'). Drives the statement, the packages note and the inquiry copy. Keep the number 10–24.
- [CREDIT] — keep a small "Template by VibeFather" link to https://vibefather.ai/t/wedding-photographer-portfolio in the footer (default: yes; delete this line to remove it)
The copy under Pages & Sections is written for the default values. If your values differ, rewrite every headline, paragraph, label and image for them — same length, tone and structure. Layout, colors, type and motion stay as described.
Keep all copy, image paths and links in one content file (for example `src/content.ts`), keyed by section, so the content can be swapped later without touching components.Pro template. The design, sections, motion and assets parts of this prompt are for Pro members. The live demo and DESIGN.md stay free.
Join Pro early access →DESIGN.md
The design system on its own — colors, type, spacing and motion. Apply this look to a project you already have.
---
name: "Wedding Photographer Portfolio"
colors:
ink_black: "#141311"
charcoal: "#1D1B18"
graphite: "#2A2723"
bone: "#E9E4DA"
milk: "#F5F2EA"
smoke: "#9A938A"
brass: "#C0A268"
brass_deep: "#A08549"
fern: "#7E8F7A"
typography:
display_h1_h2_wedding_titles_the_wordmark_wide_caps_prices_in_large_sizes_frame_numbers: { family: "Italiana", weights: [400] }
text_and_ui_body_small_caps_labels_font_feature_smcp_or_uppercase_letterspaced_buttons_forms_captions_numbers_font_variant_numeric_tabular_nums: { family: "Manrope", weights: [400, 500, 700] }
spacing: "8px base unit. Container max-width 1240px, centered, side padding 48px desktop, 32px tablet, 20px under 640px. 12-column grid, column gap 24px. Section vertical padding 120px desktop, 88px under 1024px, 72px under 640px — galleries breathe. Type scale: H1 clamp(52px, 8vw, 108px) Italiana 400 uppercase, letter-spacing 0.08em, line-height 1.0; H2 clamp(34px, 4.6vw, 60px) Italiana 400 uppercase letter-spacing 0.06em; H3 26px Italiana 400; lead Manrope 400 18px / 1.7 Bone at 88%; body Manrope 400 15.5px / 1.75 (max-width 62ch); small-caps label Manrope 700 11px uppercase letter-spacing 0.28em Smoke, flanked by 32px x 1px Brass rules at 50% opacity; caption Manrope 400 13px Smoke. Buttons: height 52px, padding 0 30px, Manrope 700 12px uppercase letter-spacing 0.22em. Primary: Bone fill, Ink Black text. Secondary: 1px Bone/40% border, Bone text (hover border Brass). Inputs: height 52px, 1px Bone/25% border, Charcoal fill, Bone text, focus border Brass. Focus ring everywhere: 2px Brass outline with 3px offset. Letterbox chrome: frames carry a 1px Bone/15% border and an 8px inset Milk/10% veil on hover only."
radius: "4px on cards, buttons, inputs, chips, frames and the inquiry panel — film corners. 999px only for the availability dot and the lightbox frame counter."
---
# Wedding Photographer Portfolio — Design System
Apply this design system to an existing project. It describes the look and feel only; for the full site, use prompt.md.
## Colors
- **Ink Black** `#141311` — Page background on all pages, lightbox chrome, footer.
- **Charcoal** `#1D1B18` — Alternate section background, cards on dark, inquiry panel.
- **Graphite** `#2A2723` — Hover surface on dark, tier card featured fill, hairline blocks.
- **Bone** `#E9E4DA` — Primary text on dark: headlines, body, buttons with dark fill.
- **Milk** `#F5F2EA` — Text on photos in letterbox bars, lightbox captions, inverse accents.
- **Smoke** `#9A938A` — Secondary text, small-caps labels, captions, disabled dates (at 50%).
- **Brass** `#C0A268` — The one accent: small-caps eyebrows, thin rules, prices, selected states, focus ring, booked-date marks.
- **Brass Deep** `#A08549` — Brass hover state, primary button pressed, link underline.
- **Fern** `#7E8F7A` — Quiet second color: open-date check mark, 'available' notes, next-story arrow hover.
## Typography
- **Display: H1, H2, wedding titles, the wordmark (wide caps), prices in large sizes, frame numbers** — Italiana (OFL), weights 400. Self-host the font files.
- **Text and UI: body, small-caps labels (font-feature 'smcp' or uppercase letterspaced), buttons, forms, captions, numbers (font-variant-numeric tabular-nums)** — Manrope (OFL), weights 400/500/700. Self-host the font files.
## Spacing & shape
- Spacing: 8px base unit. Container max-width 1240px, centered, side padding 48px desktop, 32px tablet, 20px under 640px. 12-column grid, column gap 24px. Section vertical padding 120px desktop, 88px under 1024px, 72px under 640px — galleries breathe. Type scale: H1 clamp(52px, 8vw, 108px) Italiana 400 uppercase, letter-spacing 0.08em, line-height 1.0; H2 clamp(34px, 4.6vw, 60px) Italiana 400 uppercase letter-spacing 0.06em; H3 26px Italiana 400; lead Manrope 400 18px / 1.7 Bone at 88%; body Manrope 400 15.5px / 1.75 (max-width 62ch); small-caps label Manrope 700 11px uppercase letter-spacing 0.28em Smoke, flanked by 32px x 1px Brass rules at 50% opacity; caption Manrope 400 13px Smoke. Buttons: height 52px, padding 0 30px, Manrope 700 12px uppercase letter-spacing 0.22em. Primary: Bone fill, Ink Black text. Secondary: 1px Bone/40% border, Bone text (hover border Brass). Inputs: height 52px, 1px Bone/25% border, Charcoal fill, Bone text, focus border Brass. Focus ring everywhere: 2px Brass outline with 3px offset. Letterbox chrome: frames carry a 1px Bone/15% border and an 8px inset Milk/10% veil on hover only.
- Radius: 4px on cards, buttons, inputs, chips, frames and the inquiry panel — film corners. 999px only for the availability dot and the lightbox frame counter.
## Motion
- Easing: --ease-out cubic-bezier(0.22, 0.61, 0.36, 1) for reveals and the lightbox; --ease-io cubic-bezier(0.65, 0, 0.35, 1) for the inquiry steps and FAQ. Slow, weighted, cinematic — nothing bounces.
- Reveal: blocks wrapped in <Reveal> start at opacity 0, translateY(20px) and animate to rest over 900ms --ease-out when 12% visible (Motion whileInView, viewport once). Siblings stagger by 90ms (index prop).
- Hero: the full-bleed frame scales from 1.06 to 1.0 over 3000ms --ease-out while a bottom gradient rises; the wordmark letters fade in with 40ms stagger; the small-caps line draws its rules outward from center. Scrolling translates the photo 0 to 10% and fades the overlay to 55% (Motion useScroll, offset ['start start', 'end start']).
- Wedding cards: on hover the photo scales 1.03, the letterbox caption bar rises from the bottom edge, and the place/season small-caps brighten Smoke → Bone over 400ms. The card's frame number ('01') crossfades to an ArrowUpRight.
- Story frames: each frame fades from opacity 0 with a 12px rise on scroll; full-bleed frames also scale from 1.03 to 1.0 as they settle. Captions fade 200ms after their frame.
- Lightbox: opens with a 200ms fade to Ink Black at 96%; the frame crossfades between photos over 500ms --ease-io with a subtle 8px scale settle; the counter and caption fade once. Arrow keys navigate, Escape closes, focus returns to the invoking frame. Under reduced motion: instant swap.
- Packages: the featured tier lifts 8px and carries a 1px Brass border; on hover each tier's price counts nothing but the small-caps note crossfades to the deliverable list's first line.
- Inquiry: the date check runs a 600ms 'checking' state (three dots pulsing), then answers — Fern check and 'That date is open' for free dates, Brass circle and 'Already held — the week after is open' for booked ones. Submitting slides the form out and the SentCard in (16px rise, 320ms --ease-io, AnimatePresence mode 'wait').
- FAQ accordions: height 0 to auto, opacity 0 to 1, 340ms --ease-io; Plus rotates 45 degrees into an X.
- Header: transparent over the heroes with Milk text and a bottom gradient; after 40px of scroll it gets Ink Black at 88%, backdrop-filter blur(12px), Bone text, 250ms transition. The wordmark is always centered; nav small-caps at the edges.
- Links: .link-draw grows a 1px Brass underline from 0% to 100% over 300ms on hover and focus-visible.
- prefers-reduced-motion: reduce — disable the hero scale and parallax, letter stagger, frame scales, lightbox crossfade (instant), dots and count-adjacent effects. Color and border hover changes stay. Use CSS @media in index.css plus Motion's useReducedMotion in components.
## Imagery
Real photography only, cinematic and consistent, all six made for this template in one shoot's style: available light, deep shadows, warm skinless compositions, fine film grain. Faces never appear, but people do: one couple in silhouette at golden hour joins the venues, the dresses, the couples-from-far-behind, the tablescapes and empty floors — a portrait-photographer's world with the likenesses kept private. No readable text or logos anywhere. Every photo uses object-fit cover with the object-position given under Assets, 4px corners, 1px Bone/15% border; captions in Manrope 13px Smoke under the frame, frame numbers in Italiana. Drawn in code: the favicon, the wordmark (type only), frame numbers, the availability dot, the lightbox chrome, dividers. Icons: lucide-react only, stroke width 1.5.
## Tone
Quiet, specific and a little wry, like a photographer editing at 1am. Short declarative lines, concrete numbers, no SEO mush, no 'capturing your special day'.
---
— Prompt by VibeFather · https://vibefather.ai/t/wedding-photographer-portfolio · CC BY-NC 4.0 (non-commercial; commercial use needs a VibeFather Pro license) · Request removal: https://vibefather.ai/takedown