Template
Marisol Duarte
An elegant, warm portfolio for a food photographer. A full-bleed overhead food photo sits under a translucent header and a centered serif headline, followed by an asymmetric editorial grid of dishes with ingredient-style captions and lighting notes, an infinite carousel and a validated contact form. It is built for food photographers who shoot for restaurants, brands and editors.
Editorial food photographer portfolio · Website · Portfolio · static site
A mock-up of the homepage, drawn from this design’s layout, palette and typefaces. A build follows the full prompt below.
Start from this templateRead the build prompt
Typefaces
Bodoni Moda is the high-contrast display serif the design wants in place of Boska, Cormorant's italic suits the recipe-card notes, and DM Sans stays as the friendly body.
- Bodoni ModaHeadings: hero 72px 400, section heads 48px 500 at -1.2px
- DM SansBody: body 16px, 14px ingredient captions
- CormorantAccent: italic serif lighting notes at 16px
Who it is for
- Food and still-life photographers
- Photographers pitching restaurants and food brands
- Creatives who tell stories with each image
- Editorial food stylists
Layout
- Translucent sticky header: serif name left; Home (active), Work, About, Styleguide, Contact right
- Full-bleed hero photo with soft overlay: tracked uppercase eyebrow, two-line light serif headline, one-line subtitle, thin vertical scroll line
- Intro statement
- Asymmetric 12-column editorial grid: offset images of different sizes, each with a sentence-case ingredient caption and an italic lighting note
- Full-width image breakouts
- Infinite carousel of work
- About with portrait
- Contact: 'Let's create something beautiful' with validated form
- Footer; living Styleguide page
Palette
Warm, appetizing, editorial, elegant, calm. It reads like a food magazine spread with natural light and generous margins.
- background (warm off-white)
#faf8f5 - text
#2b2b2b - accent (terracotta)
#9f563c - secondary surface
#efebe7 - muted
#e9e6e2 - border
#dedad3 - caption text
#5f5a55 - input border
#9a8e79
Every checked pair, measured again
| Sample | Where | Ratio | Needs |
|---|---|---|---|
| Aa | body text on off-white | 13.36:1 | 4.5:1 |
| Aa | caption text on off-white | 6.43:1 | 4.5:1 |
| Aa | caption text on secondary surface | 5.75:1 | 4.5:1 |
| Aa | accent links on off-white | 5.12:1 | 4.5:1 |
| Aa | accent on secondary surface | 4.57:1 | 4.5:1 |
| Aa | white text on accent button | 5.43:1 | 4.5:1 |
| Aa | white hero text on 65% overlay (worst case) | 5.40:1 | 4.5:1 |
| Aa | header text on white/80 over darkest photo | 8.82:1 | 4.5:1 |
| input border on off-white | 3.04:1 | 3:1 | |
| Aa | dark mode body text | 15.02:1 | 4.5:1 |
| Aa | dark mode accent text | 4.86:1 | 4.5:1 |
As vibld’s tokens
The palette on the fifteen colour tokens vibld styles a project with, each text colour on the fill it is read on. Marked tokens are solved from the palette, because no swatch held that role at 4.5:1.
- background
- card
- muted
- primary
- secondary
- accent
- destructive *
Type scale
- Display
- Boska serif 400, 72px hero (light, airy), 48px section headings at 500 with -1.2px tracking (Boska is not on Google Fonts; a vibld project loads Bodoni Moda)
- Body
- DM Sans 16px at 1.6 line-height; 14px sentence-case ingredient captions in #5f5a55; italic serif at 16px for lighting notes
High-contrast display serif with a friendly geometric sans; captions mimic a menu or recipe card.
Spacing and imagery
1120px container with 80px gutters, intentionally offset columns, big vertical gaps (~200px) between image rows, small 4px radius.
Bright overhead and 3/4-angle food photography (salads, pizza, burgers, pastries) on textured surfaces; hero washed with a dark warm overlay for legible text.
Components
- Translucent sticky header
- Full-bleed hero with overlay
- Tracked eyebrow label
- Serif headline
- Scroll line indicator
- Asymmetric editorial grid
- Ingredient caption + italic lighting note
- Full-width breakout image
- Infinite carousel
- About portrait section
- Validated contact form
- Styleguide page
- Mobile drawer nav
Interactions
- Images fade and rise into view on scroll with slight offsets
- Hover gently scales images and darkens caption
- Infinite carousel with drag and pause on hover
- Header gains a white/80 blurred background on scroll
- Contact form validation with inline errors and toast
- Mobile drawer navigation
Data
Shot{id, src, alt, ingredients, lighting, client?, layoutSpan}Series{slug, title, shots[]}Profile{name, city, bio, portrait}Inquiry{name, email, company, projectType, budget, message}
Build prompt
The baseline every prompt in the catalog assumes, then this design’s own ten sections, from goal to guardrails.
The baseline
### How to use these prompts Paste a template's build prompt into your coding agent as the first message. Each prompt names its own stack, tokens and acceptance criteria; the rules below apply to all of them and can be prepended once per project. ### Engineering baseline - TypeScript strict mode, no `any`, small typed components, feature folders, and one source of truth for design tokens (CSS variables consumed by Tailwind). - Validate every input with a shared zod schema on the client and again on the server or edge function. Never trust client-side checks alone. - Show loading, empty and error states for every async view. Surface errors in plain language with a retry, and log details to the console in development only. - Keep secrets out of the bundle. Only publishable keys (for example a Supabase anon key) belong in client code; service-role keys, API keys and webhooks live in server or edge-function environment variables. ### Data and auth baseline (full-stack templates) - Enable Row Level Security on every table before inserting data. Default-deny, then add owner-scoped policies (`auth.uid() = user_id`) and explicit role checks for admin views. - Store roles in a separate table checked by a security-definer function, never in a user-editable profile field. - Upload files to private storage buckets with size and MIME limits, and serve them through signed URLs. - Rate-limit public endpoints (forms, auth, AI calls) and add a honeypot field or captcha to anonymous forms. - Take payments through a hosted checkout and verify webhooks by signature. Never handle raw card data. ### Accessibility and UX baseline - Target WCAG 2.2 AA: 4.5:1 contrast for normal text and 3:1 for large text, input borders, focus rings and meaningful icons or chart lines. Every palette in this catalog lists its verified pairs; re-check with a contrast tool after any colour change. - Keep body text at 16px or larger with 1.5 line height, nothing below 12px, no light weights under 24px, and uppercase only for short labels. - Give every interactive element a visible focus ring, full keyboard support, semantic landmarks, labelled form fields, and alt text on meaningful images. - Respect `prefers-reduced-motion` for every animation. Give drag-and-drop and carousels keyboard and button alternatives. - Build mobile-first and test at 375px, 768px and 1280px. ### Content guardrails - Use original copy, fictional sample data and placeholder or licensed imagery. Do not reuse another product's name, logo, screenshots or marketing text. - Label demo testimonials and metrics as samples. Collect the minimum personal data the feature needs.
### Goal
Build a warm, editorial portfolio for a fictional food photographer, "Marisol Duarte". It targets restaurant owners, food brands and magazine editors. Each image should feel like a story: captions read like menu lines (the ingredients) and short italic notes describe the light. The site opens on a full-bleed hero, moves through an intentionally uneven grid, and ends with a friendly inquiry form.
### Stack
React 18 + TypeScript + Vite, Tailwind CSS, shadcn/ui (Radix Form, Select, Toast via Sonner) and lucide-react. Use Framer Motion for scroll reveals, Embla Carousel with loop and drag for the infinite strip, Vaul for the mobile drawer, react-hook-form + zod and React Router for Home, Work, About, Styleguide and Contact. One serverless function relays inquiries.
### Pages & layout
1. Header, 80px, sticky: white/80 with `backdrop-blur`. A serif name on the left and five links on the right; the active link is darker.
2. Hero at 100vh: a photo with a 65% #1c1a18 overlay (effective #6b6a69 or darker behind white text), an eyebrow "FOOD PHOTOGRAPHY" in 14px with 0.3em tracking, a two-line headline at clamp(40px, 6vw, 72px) in light serif, a subtitle and a 64px vertical hairline scroll cue.
3. Intro: a centered 2-sentence statement in serif.
4. Editorial grid: 12 columns. Rows pair images with different spans and offsets (for example 7 plus 5 columns, then 4 columns starting at column 2 plus 6 columns starting at column 7, dropped 64px). Below each image: a 14px sentence-case ingredients line and an italic lighting note.
5. Breakout: one full-bleed image between grid rows.
6. Carousel: a looping strip of 6-10 square thumbnails.
7. Contact: a serif heading, the form and details. Footer with socials.
### Design system
- `--background: #faf8f5`, `--foreground: #2b2b2b`, `--accent: #9f563c`, `--secondary: #efebe7`, `--muted: #e9e6e2`, `--border: #dedad3` (decorative), `--muted-foreground: #5f5a55`, `--input-border: #9a8e79`, `--radius: 4px`.
- Dark tokens: background #1c1a18, foreground #f2eee9, accent #c07459.
- Fonts: Boska (Fontshare), or Bodoni Moda or Cormorant from Google Fonts, for display. DM Sans for body. Scale: 12 / 14 / 16 / 20 / 32 / 48 / 72px.
- Spacing: 8px base, 80px gutters, 160-200px between rows.
- Motion: 700ms cubic-bezier(0.22,1,0.36,1) fade-up with a 20px offset and 400ms image hover scale (1.03).
### Components & interactions
- Shot: skeleton (warm gray), then fade in. Hover or focus scales the image. The caption is always visible.
- Carousel: drag, arrows and a pause button; it pauses on hover and focus.
- InquiryForm: name, email, company, project type select, budget select and message. States for invalid, submitting, success and error.
- Styleguide page: swatches, type ramp and buttons in all states.
### Data & state
`Shot{id, src, alt, ingredients, lighting, client?, span}` and `Series{slug, title, shots}` are typed. The grid layout comes from the `span` and `offset` fields. Seed 10-14 placeholder food photos with invented dish descriptions and no real restaurant names.
### Accessibility
- The terracotta accent is deepened to #9f563c (5.12:1 on off-white, 4.57:1 on the secondary surface) so it works for links and small text; dark mode keeps #c07459 (4.86:1). Captions use #5f5a55.
- The 65% hero overlay keeps the white eyebrow, headline and subtitle at 5.4:1 even over a white plate.
- Asymmetric layout must not scramble reading order. Keep DOM order row by row.
- Alt text should describe the dish, not the ingredients list verbatim. The carousel is pausable and honors reduced motion.
- Verified contrast: body #2b2b2b on #faf8f5 = 13.36:1; captions #5f5a55 on #faf8f5 = 6.43:1; accent #9f563c on #faf8f5 = 5.12:1; hero text #ffffff on #6b6a69 = 5.4:1.
### Security
- zod on the client and in the function, with a honeypot and rate limit.
- Store only what the inquiry needs; don't collect phone numbers unless required.
- No secrets in client-side code. External links use `rel="noopener noreferrer"`.
### Performance & SEO
The hero image is the LCP, so preload it with AVIF `srcset`. Lazy-load the grid. Add Photographer/LocalBusiness JSON-LD and OG images per series.
### Guardrails
- Do write all copy yourself and use placeholder imagery (Unsplash-style stock or generated); no trademarked names, client logos or real people.
- Don't present invented testimonials or awards as real; label sample content as sample.
- Keep components small and fully typed; no `any`. Surface errors visibly instead of failing silently.
- Don't collect or store more personal data than the contact form needs.
- Done when:
- [ ] Asymmetric grid renders intentionally offset rows and collapses cleanly on mobile
- [ ] Every image has ingredient caption and lighting note
- [ ] Carousel loops, drags, and pauses
- [ ] Inquiry form covers all states
- [ ] Accent color used only where contrast allows