Free
Contact
Emberlyn Contact
A contact block extracted from the Emberlyn template, available in Tailwind CSS and Bootstrap.
0
1
A contact block extracted from the Emberlyn template, available in Tailwind CSS and Bootstrap.
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.FontAwesomeConfig = { autoReplaceSvg: 'nest' };
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emberlyn Contact | Tailwind CSS UI Block</title>
<meta name="description" content="A contact block extracted from the Emberlyn template, available in Tailwind CSS and Bootstrap.">
<meta name="keywords" content="Emberlyn, Tailwind CSS template, HTML website template, portfolio template, wedding photographer website, photography portfolio, elopement photographer, photo studio template, editorial portfolio">
<meta name="author" content="Let's templates">
<link rel="canonical" href="">
<meta property="og:type" content="product">
<meta property="og:site_name" content="Let's templates">
<meta property="og:locale" content="en_US">
<meta property="og:url" content="">
<meta property="og:title" content="Emberlyn | Tailwind CSS Portfolio Template">
<meta property="og:description" content="A quiet, editorial Tailwind CSS portfolio template for wedding photographers, with a black-and-white hero slider, a staggered journal and a date availability calendar.">
<meta property="og:image" content="">
<meta property="og:image:alt" content="Emberlyn template preview">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="">
<meta name="twitter:title" content="Emberlyn | Tailwind CSS Portfolio Template">
<meta name="twitter:description" content="A quiet, editorial Tailwind CSS portfolio template for wedding photographers, with a black-and-white hero slider, a staggered journal and a date availability calendar.">
<meta name="twitter:image" content="">
<meta name="twitter:image:alt" content="Emberlyn template preview">
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Mrs+Saint+Delafield&family=Playfair+Display:wght@400;500&family=Poppins:wght@300;400;500&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: { paper: '#f7f6f2', sand: '#efede8', sage: { DEFAULT: '#5f6c5f', deep: '#4b564b' }, ink: '#232323', mute: '#707070' },
fontFamily: { sans: ['Poppins', 'sans-serif'], serif: ['Playfair Display', 'serif'], caps: ['Cormorant Garamond', 'serif'], script: ['Mrs Saint Delafield', 'cursive'] }
}
}
}
</script>
<style>
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: 180ms ease-out both page-fade-out; }
::view-transition-new(root) { animation: 220ms ease-in both page-fade-in; }
@keyframes page-fade-out { to { opacity: 0; } }
@keyframes page-fade-in { from { opacity: 0; } }
html { scroll-behavior: smooth; }
[hidden] { display: none !important; }
.slide { transition: opacity .7s ease, transform 6s ease; }
.slide:not(.is-active) { opacity: 0; }
.slide.is-active { transform: scale(1.04); }
.notch { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 1.25rem), calc(50% + 1.25rem) calc(100% - 1.25rem), 50% 100%, calc(50% - 1.25rem) calc(100% - 1.25rem), 0 calc(100% - 1.25rem)); }
.reveal-ready [data-reveal] { opacity: 0; transform: translateY(24px); transition: opacity .7s ease, transform .7s ease; transition-delay: var(--reveal-delay, 0ms); }
.reveal-ready [data-reveal].is-visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.slide { transition: none; }
.slide.is-active { transform: none; }
::view-transition-old(root), ::view-transition-new(root) { animation: none; }
}
</style>
</head>
<body class="bg-paper font-sans font-light text-ink antialiased">
<% active_page = "index" %>
<% left_nav = { "about" => "meet us", "story" => "our story", "services" => "services" } %>
<% right_nav = { "portfolio" => "portfolio", "blog" => "the journal", "contact" => "hire us" } %>
<% left_nav.each do |slug, label| %>
<% active = active_page == slug %>
<% end %>
<% right_nav.each do |slug, label| %>
<% active = active_page == slug %>
<% end %>
<% { "index" => "home" }.merge(left_nav).merge(right_nav).each do |slug, label| %>
<% active = active_page == slug %>
<% end %>
<% slides = [["1460978812857-470ed1c77af0", "Couple kissing on a windy shore, the bride's veil lifting in black and white"], ["1537633552985-df8429e8048b", "Bride and groom embracing by the sea, her long veil trailing on the pebbles"], ["1532712938310-34cb3982ef74", "Newlyweds walking hand in hand across a hilltop at dusk"]] %>
<% slides.each_with_index do |(photo, alt), index| %>
<% end %>
<% slides.each_index do |index| %>
<% end %>
<% [["fa-instagram", "Instagram", "https://www.instagram.com"], ["fa-facebook-f", "Facebook", "https://www.facebook.com"], ["fa-pinterest-p", "Pinterest", "https://www.pinterest.com"]].each do |icon, label, url| %>
<% end %>
<% stories = [
["03 · 02 · 2026", "Engagement", "Lena and Tomas, sunset session", "A breezy evening on the cliffs of the Algarve: four hours, one picnic and more than six hundred frames of pure, easy joy.", "1532712938310-34cb3982ef74", "Couple embracing on a hilltop at golden hour", false],
["14 · 06 · 2025", "Wedding", "Amelia and Noah", "A June wedding on a vineyard terrace. They decided to marry three months after meeting, and every photo shows why.", "1519741497674-611481863552", "Bride and groom backlit by the evening sun", true],
["21 · 09 · 2025", "Wedding", "Love is in the air", "I never feel like I'm at work at a wedding. Here's a celebration that turned strangers into one big family by midnight.", "1583939003579-730e3918a45a", "Guests throwing confetti as the couple kisses", true],
["28 · 02 · 2026", "Couples · Engagement", "Inès and Rafael", "What a wonderfully easy-going pair these two are.", "1591604466107-ec97de577aff", "Couple smiling together beside an autumn lake", false]
] %>
<% stories.each_with_index do |(date, category, title, excerpt, photo, alt, tall), index| %>
<% end %>
<% collections = [["Weddings", "48 stories", "Full days, from getting ready to the last dance", "1511285560929-80b456fea0bc"], ["Elopements", "21 stories", "Just the two of you, somewhere wild", "1537633552985-df8429e8048b"], ["Engagements", "33 stories", "Easy sessions before the big day", "1520854221256-17451cc331bf"], ["Details", "60+ sets", "Rings, flowers, tables and handwritten vows", "1515934751635-c81c6bc9a2d8"]] %>
<% collections.each_with_index do |(title, count, text, photo), index| %>
<% end %>
<% months = (0...6).map { |offset| Date.new(2027, 4, 1) >> offset } %>
<% booked = ->(day) { (day.saturday? && (day.day % 3 != 0)) || ((day.day * day.month) % 11).zero? } %>
<!-- block:start contact -->
<section class="bg-paper" data-availability>
<div class="mx-auto grid max-w-6xl gap-12 px-5 py-20 md:px-10 md:py-28 lg:grid-cols-[1fr_1.15fr] lg:items-center">
<div>
<p class="text-[11px] uppercase tracking-[0.5em] text-mute">season 2027</p>
<h2 class="mt-3 font-serif text-4xl leading-tight md:text-5xl">Is your date still free?</h2>
<p class="mt-6 max-w-md text-sm leading-[2] text-ink/75">I take one wedding a day and around thirty a year. Pick your date to see if it's open, then hold it for seven days while we talk.</p>
<div class="mt-10 border-l border-ink/40 pl-6" aria-live="polite">
<p data-availability-summary class="font-serif text-2xl">Choose a day in the calendar</p>
<p data-availability-note class="mt-2 text-sm text-mute">Saturdays go first, so book early for summer.</p>
</div>
<a href="contact.html" data-availability-cta class="mt-8 inline-block bg-sage px-9 py-3 text-[11px] uppercase tracking-[0.4em] text-white transition hover:bg-sage-deep">Hold my date</a>
</div>
<div class="bg-white p-6 shadow-sm md:p-10">
<div class="flex items-center justify-between">
<button type="button" data-month-prev class="grid h-10 w-10 place-items-center border border-ink/20 transition hover:border-ink disabled:opacity-30" aria-label="Previous month"><i class="fa-solid fa-chevron-left text-xs" aria-hidden="true"></i></button>
<p class="font-caps text-2xl font-medium" data-month-label aria-live="polite"><%= months.first.strftime("%B %Y") %></p>
<button type="button" data-month-next class="grid h-10 w-10 place-items-center border border-ink/20 transition hover:border-ink disabled:opacity-30" aria-label="Next month"><i class="fa-solid fa-chevron-right text-xs" aria-hidden="true"></i></button>
</div>
<% months.each_with_index do |month, index| %>
<div data-month="<%= month.strftime("%B %Y") %>" <%= 'hidden' unless index.zero? %> class="mt-8">
<div class="grid grid-cols-7 gap-1 text-center text-[10px] uppercase tracking-[0.2em] text-mute" aria-hidden="true">
<% %w[Mo Tu We Th Fr Sa Su].each do |name| %><span><%= name %></span><% end %>
</div>
<div class="mt-3 grid grid-cols-7 gap-1">
<% (month.cwday - 1).times do %><span aria-hidden="true"></span><% end %>
<% (month..(month.next_month - 1)).each do |day| %>
<% taken = booked.call(day) %>
<button type="button" data-day="<%= day.strftime("%A, %-d %B %Y") %>" <%= 'disabled' if taken %> aria-pressed="false" aria-label="<%= day.strftime("%A %-d %B") %><%= taken ? ', booked' : ', available' %>" class="aspect-square text-sm transition enabled:hover:bg-sand disabled:cursor-not-allowed disabled:text-ink/25 disabled:line-through aria-pressed:bg-ink aria-pressed:text-white <%= 'font-medium' if day.saturday? && !taken %>"><%= day.day %></button>
<% end %>
</div>
</div>
<% end %>
<p class="mt-6 flex flex-wrap gap-6 text-xs text-mute"><span class="flex items-center gap-2"><span class="h-3 w-3 bg-ink"></span>your pick</span><span class="flex items-center gap-2"><span class="text-ink/25 line-through">12</span>booked</span></p>
</div>
</div>
</section>
<!-- block:end contact -->
<script>
document.querySelectorAll('[data-menu-toggle]').forEach((button) => {
const menu = document.getElementById(button.getAttribute('aria-controls'));
if (!menu) return;
button.addEventListener('click', () => {
const open = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!open));
menu.classList.toggle('hidden', open);
});
});
document.querySelectorAll('[data-slider]').forEach((slider) => {
const slides = [...slider.querySelectorAll('[data-slide]')];
const dots = [...slider.querySelectorAll('[data-slider-dot]')];
let current = 0;
let timer;
const show = (index) => {
current = (index + slides.length) % slides.length;
slides.forEach((slide, i) => {
slide.classList.toggle('is-active', i === current);
slide.toggleAttribute('aria-hidden', i !== current);
});
dots.forEach((dot, i) => dot.setAttribute('aria-pressed', String(i === current)));
};
const restart = () => {
clearInterval(timer);
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) timer = setInterval(() => show(current + 1), 6000);
};
slider.querySelector('[data-slider-prev]')?.addEventListener('click', () => { show(current - 1); restart(); });
slider.querySelector('[data-slider-next]')?.addEventListener('click', () => { show(current + 1); restart(); });
dots.forEach((dot, i) => dot.addEventListener('click', () => { show(i); restart(); }));
restart();
});
document.querySelectorAll('[data-carousel]').forEach((carousel) => {
const slides = [...carousel.querySelectorAll('[data-carousel-slide]')];
const index = carousel.querySelector('[data-carousel-index]');
let current = 0;
const show = (next) => {
current = (next + slides.length) % slides.length;
slides.forEach((slide, i) => { slide.hidden = i !== current; });
if (index) index.textContent = current + 1;
};
carousel.querySelector('[data-carousel-prev]')?.addEventListener('click', () => show(current - 1));
carousel.querySelector('[data-carousel-next]')?.addEventListener('click', () => show(current + 1));
});
document.querySelectorAll('[data-hover-list]').forEach((list) => {
const preview = list.querySelector('[data-hover-preview]');
if (!preview || !matchMedia('(hover: hover)').matches) return;
list.querySelectorAll('[data-hover-item]').forEach((item) => {
item.addEventListener('mouseenter', () => { preview.src = item.dataset.image; preview.style.opacity = '1'; });
item.addEventListener('mouseleave', () => { preview.style.opacity = '0'; });
item.addEventListener('mousemove', (event) => { preview.style.transform = `translate(${event.clientX + 28}px, ${event.clientY - 90}px) rotate(-3deg)`; });
});
});
document.querySelectorAll('[data-availability]').forEach((root) => {
const months = [...root.querySelectorAll('[data-month]')];
const label = root.querySelector('[data-month-label]');
const prev = root.querySelector('[data-month-prev]');
const next = root.querySelector('[data-month-next]');
const summary = root.querySelector('[data-availability-summary]');
const note = root.querySelector('[data-availability-note]');
let current = 0;
const showMonth = (index) => {
current = Math.max(0, Math.min(months.length - 1, index));
months.forEach((month, i) => { month.hidden = i !== current; });
label.textContent = months[current].dataset.month;
prev.disabled = current === 0;
next.disabled = current === months.length - 1;
};
prev.addEventListener('click', () => showMonth(current - 1));
next.addEventListener('click', () => showMonth(current + 1));
root.addEventListener('click', (event) => {
const day = event.target.closest('[data-day]');
if (!day || day.disabled) return;
root.querySelectorAll('[data-day]').forEach((other) => other.setAttribute('aria-pressed', String(other === day)));
summary.textContent = `${day.dataset.day} is free`;
note.textContent = 'Lovely choice. Hold it now and I will keep it for you for seven days.';
});
showMonth(0);
});
document.querySelectorAll('[data-filter-group]').forEach((group) => {
const items = document.querySelectorAll(group.dataset.filterGroup);
group.querySelectorAll('[data-filter]').forEach((chip) => {
chip.addEventListener('click', () => {
group.querySelectorAll('[data-filter]').forEach((other) => other.setAttribute('aria-pressed', String(other === chip)));
items.forEach((item) => { item.hidden = chip.dataset.filter !== 'all' && item.dataset.role !== chip.dataset.filter; });
});
});
});
document.querySelectorAll('[data-lightbox]').forEach((dialog) => {
const image = dialog.querySelector('img');
const caption = dialog.querySelector('[data-lightbox-caption]');
document.querySelectorAll('[data-lightbox-open]').forEach((button) => {
button.addEventListener('click', () => {
image.src = button.dataset.full;
image.alt = button.querySelector('img')?.alt || '';
caption.textContent = button.dataset.caption;
dialog.showModal();
});
});
dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); });
});
document.querySelectorAll('[data-contact-form]').forEach((form) => {
form.addEventListener('submit', (event) => {
event.preventDefault();
const status = form.querySelector('[data-form-status]');
if (!form.checkValidity()) {
status.textContent = form.dataset.error;
form.querySelector(':invalid')?.focus();
return;
}
status.textContent = form.dataset.success;
form.reset();
});
});
document.querySelectorAll('[data-current-year]').forEach((el) => { el.textContent = new Date().getFullYear(); });
(() => {
const items = document.querySelectorAll('[data-reveal]');
const skip = navigator.webdriver || !('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!items.length || skip) return;
document.documentElement.classList.add('reveal-ready');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
});
}, { rootMargin: '0px 0px -10% 0px' });
items.forEach((item) => observer.observe(item));
})();
</script>
</body>
</html>
Portfolio
Unlock the entire library of premium templates and blocks. Renews automatically, cancel anytime.