Tailwind UI Pattern Registry for humans and agents

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 &amp; 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 &amp; 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 &amp; 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 &amp; 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">&euro;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">&euro;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">&euro;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">&copy; 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.