portfolio photographer photography creative editorial moody elegant
Portfolio Photographer
Moody photographer portfolio with masonry grid, full-viewport hero, services with pricing, and dark contact section.
elegant Responsive Vanilla JS
Live Preview
Sections
navbarheroportfolio-gridaboutservicescontactfooter
Patterns used
HTML
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Elena Vasquez — Documentary & Portrait Photography</title>
<meta name="description" content="Documentary and portrait photographer based in Barcelona. Real moments, natural light, honest storytelling." />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://webspire.de/webspire-tokens.css">
<link rel="stylesheet" href="https://webspire.de/webspire-components.css">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Inter:wght@300;400;500&display=swap" rel="stylesheet" />
<style>
body { font-family: 'Inter', system-ui, sans-serif; color: #2d2a26; }
.font-serif { font-family: 'Playfair Display', Georgia, serif; }
/* Brand tokens */
:root {
--ws-color-bg: oklch(0.98 0.005 75);
--ws-color-bg-warm: oklch(0.97 0.015 75);
--ws-color-accent: oklch(0.42 0.17 355);
--ws-color-text: oklch(0.22 0.02 75);
--ws-color-muted: oklch(0.55 0.02 75);
}
/* Masonry layout */
.masonry { columns: 3; column-gap: 1rem; }
.masonry-item { break-inside: avoid; margin-bottom: 1rem; }
@media (max-width: 1023px) { .masonry { columns: 2; } }
@media (max-width: 639px) { .masonry { columns: 1; } }
/* interactions/thumbnail-hover-scrim */
.thumbnail-hover-scrim {
--scrim-color: oklch(0.15 0.01 260 / 0.5);
--scrim-speed: 0.3s;
--thumb-radius: 0.125rem;
position: relative;
overflow: hidden;
clip-path: inset(0 0 0 0 round var(--thumb-radius));
transform: translate3d(0, 0, 0);
}
.thumbnail-hover-scrim img {
transition: transform var(--scrim-speed) ease;
}
.thumbnail-hover-scrim::after {
content: "";
position: absolute;
inset: 0;
background: var(--scrim-color);
opacity: 0;
transition: opacity var(--scrim-speed) ease;
z-index: 1;
}
.thumbnail-hover-scrim:hover img {
transform: scale(1.05);
}
.thumbnail-hover-scrim:hover::after {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.thumbnail-hover-scrim img,
.thumbnail-hover-scrim::after { transition: none; }
}
/* Photo placeholder overlay (repurposed scrim pattern) */
.photo-placeholder { position: relative; overflow: hidden; }
.photo-placeholder .overlay {
position: absolute; inset: 0;
background: linear-gradient(to top, oklch(0 0 0 / 0.7) 0%, transparent 60%);
opacity: 0; transition: opacity 0.4s ease;
display: flex; align-items: flex-end; padding: 1.5rem;
z-index: 1;
}
.photo-placeholder:hover .overlay { opacity: 1; }
/* Scroll indicator */
.scroll-indicator { animation: bounce-down 2s ease infinite; }
@keyframes bounce-down {
0%, 100% { transform: translateY(0); opacity: 0.6; }
50% { transform: translateY(10px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.scroll-indicator { animation: none; }
}
nav.scrolled { background: oklch(0.14 0.01 75 / 0.95); backdrop-filter: blur(8px); }
</style>
</head>
<body style="background-color: var(--ws-color-bg);">
<!-- Navbar -->
<nav id="navbar" class="ws-navbar fixed top-0 left-0 right-0 z-50 transition-all duration-500">
<div class="max-w-7xl mx-auto px-6 py-5 flex items-center justify-between">
<a href="#" class="font-serif text-xl text-white tracking-wide">Elena Vasquez</a>
<div class="hidden md:flex items-center gap-8">
<a href="#portfolio" class="text-white/80 hover:text-white text-sm tracking-widest uppercase transition-colors">Portfolio</a>
<a href="#about" class="text-white/80 hover:text-white text-sm tracking-widest uppercase transition-colors">About</a>
<a href="#services" class="text-white/80 hover:text-white text-sm tracking-widest uppercase transition-colors">Services</a>
<a href="#contact" class="text-white/80 hover:text-white text-sm tracking-widest uppercase transition-colors">Contact</a>
</div>
<button id="menu-toggle" class="md:hidden text-white" aria-label="Open menu" aria-expanded="false">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
</div>
<!-- Mobile menu -->
<div id="mobile-menu" class="hidden md:hidden bg-neutral-900/95 backdrop-blur-md px-6 pb-6">
<a href="#portfolio" class="block py-3 text-white/80 hover:text-white text-sm tracking-widest uppercase border-b border-white/10">Portfolio</a>
<a href="#about" class="block py-3 text-white/80 hover:text-white text-sm tracking-widest uppercase border-b border-white/10">About</a>
<a href="#services" class="block py-3 text-white/80 hover:text-white text-sm tracking-widest uppercase border-b border-white/10">Services</a>
<a href="#contact" class="block py-3 text-white/80 hover:text-white text-sm tracking-widest uppercase">Contact</a>
</div>
</nav>
<!-- Hero -->
<section class="ws-hero relative min-h-screen flex items-center justify-center overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-neutral-900 via-neutral-800 to-stone-900"></div>
<div class="absolute inset-0 bg-black/40"></div>
<div class="relative z-10 text-center px-6">
<h1 class="font-serif text-6xl sm:text-7xl md:text-8xl lg:text-9xl text-white tracking-tight leading-none">Elena Vasquez</h1>
<p class="mt-6 text-white/60 text-lg sm:text-xl tracking-[0.3em] uppercase font-light">Documentary & Portrait Photography</p>
</div>
<div class="absolute bottom-10 left-1/2 -translate-x-1/2 z-10 scroll-indicator">
<svg class="w-5 h-8 text-white/50" fill="none" stroke="currentColor" viewBox="0 0 20 32"><rect x="1" y="1" width="18" height="30" rx="9" stroke-width="1.5"/><circle cx="10" cy="10" r="2" fill="currentColor"/></svg>
</div>
</section>
<!-- Portfolio Grid -->
<section id="portfolio" class="ws-gallery py-24 sm:py-32 px-6">
<div class="max-w-7xl mx-auto">
<h2 class="font-serif text-4xl sm:text-5xl text-center mb-4">Selected Work</h2>
<p class="text-center mb-12 max-w-xl mx-auto" style="color: var(--ws-color-muted);">A curated selection of stories told through light, shadow, and the quiet moments in between.</p>
<!-- Filter Pills -->
<div class="flex flex-wrap justify-center gap-3 mb-14">
<span class="px-5 py-2 bg-neutral-900 text-white text-xs tracking-widest uppercase rounded-full cursor-pointer">All</span>
<span class="px-5 py-2 bg-transparent text-neutral-500 text-xs tracking-widest uppercase rounded-full border border-neutral-300 hover:border-neutral-900 hover:text-neutral-900 transition-colors cursor-pointer">Documentary</span>
<span class="px-5 py-2 bg-transparent text-neutral-500 text-xs tracking-widest uppercase rounded-full border border-neutral-300 hover:border-neutral-900 hover:text-neutral-900 transition-colors cursor-pointer">Portrait</span>
<span class="px-5 py-2 bg-transparent text-neutral-500 text-xs tracking-widest uppercase rounded-full border border-neutral-300 hover:border-neutral-900 hover:text-neutral-900 transition-colors cursor-pointer">Street</span>
<span class="px-5 py-2 bg-transparent text-neutral-500 text-xs tracking-widest uppercase rounded-full border border-neutral-300 hover:border-neutral-900 hover:text-neutral-900 transition-colors cursor-pointer">Editorial</span>
</div>
<!-- Masonry Grid -->
<div class="masonry">
<!-- 1: Portrait — warm charcoal -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 3/4; background: linear-gradient(145deg, #4a4543, #3d3835);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">The Fisherman's Dawn</span></div>
</div>
</div>
<!-- 2: Landscape — dusty sage -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 16/10; background: linear-gradient(135deg, #8a9a7e, #6b7d60);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">Andalusian Light</span></div>
</div>
</div>
<!-- 3: Square — muted rose -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 1/1; background: linear-gradient(150deg, #a0757a, #8b5e63);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">Maria, Age 84</span></div>
</div>
</div>
<!-- 4: Tall portrait — deep warm -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 2/3; background: linear-gradient(160deg, #5c4f42, #3e342a);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">Smoke & Silence</span></div>
</div>
</div>
<!-- 5: Wide landscape — warm gray -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 16/9; background: linear-gradient(130deg, #7a7570, #5e5954);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">The Last Café</span></div>
</div>
</div>
<!-- 6: Portrait — amber tone -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 3/4; background: linear-gradient(145deg, #9a8264, #7d6847);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">Barcelona, 6AM</span></div>
</div>
</div>
<!-- 7: Landscape — cool stone -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 3/2; background: linear-gradient(140deg, #6e6d6a, #545350);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">Hands That Built</span></div>
</div>
</div>
<!-- 8: Square — deep olive -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 1/1; background: linear-gradient(155deg, #6b6e56, #4e513d);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">Still Life, Moving</span></div>
</div>
</div>
<!-- 9: Portrait — warm shadow -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 4/5; background: linear-gradient(140deg, #635750, #4a3f39);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">The Dancer Rests</span></div>
</div>
</div>
<!-- 10: Landscape — dusty rose -->
<div class="masonry-item">
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm cursor-pointer" style="aspect-ratio: 16/10; background: linear-gradient(135deg, #8c6b6e, #6e5255);">
<div class="overlay rounded-sm"><span class="text-white font-serif text-lg">Traces of Home</span></div>
</div>
</div>
</div>
</div>
</section>
<!-- About -->
<section id="about" class="ws-hero py-24 sm:py-32" style="background-color: var(--ws-color-bg-warm);">
<div class="max-w-7xl mx-auto px-6">
<div class="grid lg:grid-cols-2 gap-16 lg:gap-24 items-center">
<!-- Portrait placeholder -->
<div class="photo-placeholder thumbnail-hover-scrim rounded-sm" style="aspect-ratio: 3/4; background: linear-gradient(160deg, #5c524a, #3e3630);">
</div>
<!-- Text -->
<div>
<h2 class="font-serif text-4xl sm:text-5xl mb-8">About Elena</h2>
<p class="leading-relaxed mb-6" style="color: var(--ws-color-muted);">
I believe the most powerful photographs are the ones you almost didn't take. The half-glance, the hand reaching for a door, the light that lasts exactly three seconds before it's gone. My work lives in documentary and portrait photography — not because I chose the genre, but because it chose me. I follow natural light and real moments, never staged, never interrupted.
</p>
<p class="leading-relaxed mb-10" style="color: var(--ws-color-muted);">
Over fifteen years, I've moved through conflict zones and quiet villages alike, always looking for the same thing: the story a person carries in their posture, their silence, their worn-out shoes. Every session begins with listening. The camera comes second.
</p>
<div class="border-t border-neutral-200 pt-8">
<p class="text-xs tracking-widest uppercase text-neutral-400 mb-4">Featured & Recognized</p>
<div class="flex flex-wrap gap-x-8 gap-y-3">
<span class="text-neutral-700 font-medium text-sm">TIME Magazine</span>
<span class="text-neutral-300">|</span>
<span class="text-neutral-700 font-medium text-sm">National Geographic</span>
<span class="text-neutral-300">|</span>
<span class="text-neutral-700 font-medium text-sm">Sony World Photography Awards</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Services -->
<section id="services" class="ws-pricing py-24 sm:py-32 px-6">
<div class="max-w-5xl mx-auto">
<h2 class="font-serif text-4xl sm:text-5xl text-center mb-4">Services</h2>
<p class="text-center mb-16 max-w-lg mx-auto" style="color: var(--ws-color-muted);">Thoughtful photography for people who value substance over spectacle.</p>
<div class="grid md:grid-cols-3 gap-8">
<!-- Wedding -->
<div class="group border border-neutral-200 rounded-sm p-8 hover:border-neutral-400 transition-colors">
<h3 class="font-serif text-2xl mb-4">Wedding</h3>
<p class="text-sm leading-relaxed mb-8" style="color: var(--ws-color-muted);">
Unscripted coverage of your day as it unfolds. No checklists, no forced poses — just the real story of two people and everyone who came to celebrate them.
</p>
<p class="text-xs tracking-widest uppercase text-neutral-400 mb-2">Starting from</p>
<p class="font-serif text-2xl text-neutral-800 mb-6">€2,500</p>
<a href="#contact" class="text-sm tracking-widest uppercase text-neutral-900 border-b border-neutral-900 pb-0.5 transition-colors hover:text-[oklch(0.42_0.17_355)] hover:border-[oklch(0.42_0.17_355)]">Inquire</a>
</div>
<!-- Editorial -->
<div class="group border border-neutral-200 rounded-sm p-8 hover:border-neutral-400 transition-colors">
<h3 class="font-serif text-2xl mb-4">Editorial</h3>
<p class="text-sm leading-relaxed mb-8" style="color: var(--ws-color-muted);">
Magazine-ready portraits and visual narratives. Collaboration with art directors, stylists, and subjects who want to be seen — not just photographed.
</p>
<p class="text-xs tracking-widest uppercase text-neutral-400 mb-2">Starting from</p>
<p class="font-serif text-2xl text-neutral-800 mb-6">€1,800</p>
<a href="#contact" class="text-sm tracking-widest uppercase text-neutral-900 border-b border-neutral-900 pb-0.5 transition-colors hover:text-[oklch(0.42_0.17_355)] hover:border-[oklch(0.42_0.17_355)]">Inquire</a>
</div>
<!-- Commercial -->
<div class="group border border-neutral-200 rounded-sm p-8 hover:border-neutral-400 transition-colors">
<h3 class="font-serif text-2xl mb-4">Commercial</h3>
<p class="text-sm leading-relaxed mb-8" style="color: var(--ws-color-muted);">
Brand photography with a documentary eye. For companies that want to show who they are, not who they think they should be. Authentic, not corporate.
</p>
<p class="text-xs tracking-widest uppercase text-neutral-400 mb-2">Starting from</p>
<p class="font-serif text-2xl text-neutral-800 mb-6">€3,200</p>
<a href="#contact" class="text-sm tracking-widest uppercase text-neutral-900 border-b border-neutral-900 pb-0.5 transition-colors hover:text-[oklch(0.42_0.17_355)] hover:border-[oklch(0.42_0.17_355)]">Inquire</a>
</div>
</div>
</div>
</section>
<!-- Contact -->
<section id="contact" class="ws-contact py-24 sm:py-32 bg-neutral-900 text-white">
<div class="max-w-5xl mx-auto px-6">
<div class="grid lg:grid-cols-2 gap-16 lg:gap-24">
<!-- Left: Info -->
<div>
<h2 class="font-serif text-4xl sm:text-5xl mb-6">Let's Create Together</h2>
<p class="text-neutral-400 leading-relaxed mb-10">Every project starts with a conversation. Tell me about the story you want to tell, and we'll figure out the rest.</p>
<div class="space-y-6">
<div>
<p class="text-xs tracking-widest uppercase text-neutral-500 mb-2">Email</p>
<a href="mailto:hello@elenavasquez.com" class="text-xl hover:text-neutral-200 transition-colors">hello@elenavasquez.com</a>
</div>
<div>
<p class="text-xs tracking-widest uppercase text-neutral-500 mb-2">Instagram</p>
<a href="#" class="text-xl hover:text-neutral-200 transition-colors">@elenavasquez</a>
</div>
<div>
<p class="text-xs tracking-widest uppercase text-neutral-500 mb-2">Location</p>
<p class="text-lg text-neutral-300">Based in Barcelona, available worldwide</p>
</div>
</div>
</div>
<!-- Right: Form -->
<form class="ws-forms space-y-6" onsubmit="return false;">
<div>
<label for="name" class="block text-xs tracking-widest uppercase text-neutral-500 mb-2">Name</label>
<input type="text" id="name" name="name" required class="w-full bg-transparent border-b border-neutral-700 focus:border-white py-3 text-white placeholder-neutral-600 outline-none transition-colors" placeholder="Your name" />
</div>
<div>
<label for="email" class="block text-xs tracking-widest uppercase text-neutral-500 mb-2">Email</label>
<input type="email" id="email" name="email" required class="w-full bg-transparent border-b border-neutral-700 focus:border-white py-3 text-white placeholder-neutral-600 outline-none transition-colors" placeholder="your@email.com" />
</div>
<div>
<label for="project-type" class="block text-xs tracking-widest uppercase text-neutral-500 mb-2">Project Type</label>
<select id="project-type" name="project-type" class="w-full bg-transparent border-b border-neutral-700 focus:border-white py-3 text-white outline-none transition-colors cursor-pointer appearance-none">
<option value="" class="bg-neutral-900">Select a project type</option>
<option value="wedding" class="bg-neutral-900">Wedding</option>
<option value="editorial" class="bg-neutral-900">Editorial</option>
<option value="commercial" class="bg-neutral-900">Commercial</option>
<option value="personal" class="bg-neutral-900">Personal Portrait</option>
<option value="other" class="bg-neutral-900">Something Else</option>
</select>
</div>
<div>
<label for="message" class="block text-xs tracking-widest uppercase text-neutral-500 mb-2">Message</label>
<textarea id="message" name="message" rows="4" class="w-full bg-transparent border-b border-neutral-700 focus:border-white py-3 text-white placeholder-neutral-600 outline-none transition-colors resize-none" placeholder="Tell me about your project..."></textarea>
</div>
<button type="submit" class="mt-4 px-8 py-3 bg-white text-neutral-900 text-sm tracking-widest uppercase hover:bg-neutral-100 transition-colors">Send Inquiry</button>
</form>
</div>
</div>
</section>
<!-- Footer -->
<footer class="ws-footer bg-neutral-950 text-neutral-500 py-10 px-6">
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<p class="text-sm">© 2026 Elena Vasquez. All rights reserved.</p>
<div class="flex items-center gap-6">
<a href="#" class="text-sm hover:text-white transition-colors" aria-label="Instagram">Instagram</a>
<a href="#" class="text-sm hover:text-white transition-colors">Privacy</a>
<a href="#" class="text-sm hover:text-white transition-colors">Imprint</a>
</div>
</div>
</footer>
<!-- Minimal JS: mobile menu + navbar scroll -->
<script>
const toggle = document.getElementById('menu-toggle');
const menu = document.getElementById('mobile-menu');
toggle.addEventListener('click', () => {
const open = !menu.classList.contains('hidden');
menu.classList.toggle('hidden');
toggle.setAttribute('aria-expanded', !open);
});
menu.querySelectorAll('a').forEach(a => a.addEventListener('click', () => {
menu.classList.add('hidden');
toggle.setAttribute('aria-expanded', 'false');
}));
const nav = document.getElementById('navbar');
window.addEventListener('scroll', () => {
nav.classList.toggle('scrolled', window.scrollY > 80);
}, { passive: true });
</script>
</body>
</html>
Photographer portfolio with editorial mood. Dark hero, warm tones, masonry grid. The template disappears — the work stands front and center.