Contact Form with Hero Banner
ContactContact page with a full-bleed background-image banner, an overlapping card-style form, and a four-column contact info grid below.
Create a Svelte 5 (runes) contact section. A full-width banner div uses a background image with a dark linear-gradient overlay (`linear-gradient(rgba(17,24,39,0.65),rgba(17,24,39,0.65))` composited over the image via `background-image`), tall bottom padding (pb-72 lg:pb-80) to make room for the form to overlap it, containing a centered shimmering-text heading and light subtext. Below the banner, a contact form card is pulled up over the banner with a large negative top margin (-mt-96), centered, max-w-screen-md, rounded, bordered, with a 2-column grid of labeled inputs (first name, last name, email, phone) plus a full-width textarea for the message, a privacy-policy disclaimer paragraph with two inline links, and a submit button wrapped in a subtle looping shine/sheen hover effect. Below the form, a 4-column responsive grid of contact info cards (Email, Phone, Support, Address), each with a small icon in a muted square badge, a title, description, and either a mailto/tel link, a list of phone numbers, a button-style link, or a Google Maps link for the address. All copy and links are data-driven via a single `content` prop matching: `{ heading, subtext, bg_image, form_action, fields: {first_name,last_name,email,phone,message}, submit_label, privacy: {text,terms_label,terms_href,policy_label,policy_href,suffix}, contact_email, contact_phone, contact_support, contact_address }`.
<script lang="ts">
import ShimmeringText from '$lib/components/animate-ui/ShimmeringText.svelte';
import Shine from '$lib/components/animate-ui/Shine.svelte';
import { reveal } from '$lib/actions/reveal';
type Props = {
content: {
heading: string;
subtext: string;
bg_image: string;
form_action: string;
fields: {
first_name: { label: string; placeholder: string };
last_name: { label: string; placeholder: string };
email: { label: string; placeholder: string };
phone: { label: string; placeholder: string };
message: { label: string; placeholder: string };
};
submit_label: string;
privacy: { text: string; terms_label: string; terms_href: string; policy_label: string; policy_href: string; suffix: string };
contact_email: { title: string; description: string; email: string };
contact_phone: { title: string; description: string; phones: string[] };
contact_support: { title: string; description: string; label: string; href: string };
contact_address: { title: string; description: string; address: string };
};
};
let { content }: Props = $props();
const inputClass = 'block w-full rounded-lg border border-input bg-muted p-3 text-sm text-foreground placeholder:text-muted-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-ring';
</script>
<section class="bg-background">
<div
class="bg-no-repeat bg-cover bg-center"
style="background-image: linear-gradient(rgba(17,24,39,0.65),rgba(17,24,39,0.65)), url('{content.bg_image}')"
>
<div class="px-4 lg:pt-24 pt-8 pb-72 lg:pb-80 mx-auto max-w-screen-sm text-center lg:px-6" use:reveal={{ animation: 'up' }}>
<h2 class="mb-4 text-4xl tracking-tight font-extrabold">
<ShimmeringText text={content.heading} color="oklch(0.95 0.01 240)" shimmeringColor="var(--primary)" />
</h2>
<p class="mb-16 font-light text-white/70 sm:text-xl">{content.subtext}</p>
</div>
</div>
<div class="py-16 px-4 mx-auto -mt-96 max-w-screen-xl sm:py-24 lg:px-6">
<form
action={content.form_action}
class="grid grid-cols-1 gap-8 p-6 mx-auto mb-16 max-w-screen-md bg-card rounded-lg border border-border shadow-sm lg:mb-28 sm:grid-cols-2"
use:reveal={{ animation: 'up', delay: 100 }}
>
<div>
<label for="first-name" class="block mb-2 text-sm font-medium text-foreground">
{content.fields.first_name.label}
</label>
<input type="text" id="first-name" class={inputClass} placeholder={content.fields.first_name.placeholder} required />
</div>
<div>
<label for="last-name" class="block mb-2 text-sm font-medium text-foreground">
{content.fields.last_name.label}
</label>
<input type="text" id="last-name" class={inputClass} placeholder={content.fields.last_name.placeholder} required />
</div>
<div>
<label for="contact-email" class="block mb-2 text-sm font-medium text-foreground">
{content.fields.email.label}
</label>
<input type="email" id="contact-email" class={inputClass} placeholder={content.fields.email.placeholder} required />
</div>
<div>
<label for="phone-number" class="block mb-2 text-sm font-medium text-foreground">
{content.fields.phone.label}
</label>
<input type="tel" id="phone-number" class={inputClass} placeholder={content.fields.phone.placeholder} required />
</div>
<div class="sm:col-span-2">
<label for="message" class="block mb-2 text-sm font-medium text-foreground">
{content.fields.message.label}
</label>
<textarea id="message" rows={6} class={inputClass} placeholder={content.fields.message.placeholder}></textarea>
<p class="mt-4 text-sm text-muted-foreground">
{content.privacy.text}
<a href={content.privacy.terms_href} class="text-primary hover:underline">{content.privacy.terms_label}</a>
{' '}and our{' '}
<a href={content.privacy.policy_href} class="text-primary hover:underline">{content.privacy.policy_label}</a>
{' '}{content.privacy.suffix}
</p>
</div>
<Shine loop loopDelay={3000} color="#ffffff" opacity={0.4} class="rounded-lg sm:w-fit">
<button
type="submit"
class="py-3 px-5 text-sm font-medium text-primary-foreground rounded-lg bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring"
>
{content.submit_label}
</button>
</Shine>
</form>
<div class="space-y-8 text-center md:grid md:grid-cols-2 lg:grid-cols-4 md:gap-12 md:space-y-0" use:reveal={{ animation: 'up', delay: 200 }}>
<div>
<div class="flex justify-center items-center mx-auto mb-4 w-10 h-10 bg-muted rounded-lg lg:h-16 lg:w-16">
<svg class="w-5 h-5 text-muted-foreground lg:w-8 lg:h-8" fill="currentColor" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
</div>
<p class="mb-2 text-xl font-bold text-foreground">{content.contact_email.title}</p>
<p class="mb-3 text-muted-foreground">{content.contact_email.description}</p>
<a href="mailto:{content.contact_email.email}" class="font-semibold text-primary hover:underline">
{content.contact_email.email}
</a>
</div>
<div>
<div class="flex justify-center items-center mx-auto mb-4 w-10 h-10 bg-muted rounded-lg lg:h-16 lg:w-16">
<svg class="w-5 h-5 text-muted-foreground lg:w-8 lg:h-8" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
</svg>
</div>
<p class="mb-2 text-xl font-bold text-foreground">{content.contact_phone.title}</p>
<p class="mb-3 text-muted-foreground">{content.contact_phone.description}</p>
<div class="flex flex-col gap-1">
{#each content.contact_phone.phones as phone}
<a href="tel:{phone.replace(/\s/g, '')}" class="font-semibold text-primary hover:underline">{phone}</a>
{/each}
</div>
</div>
<div>
<div class="flex justify-center items-center mx-auto mb-4 w-10 h-10 bg-muted rounded-lg lg:h-16 lg:w-16">
<svg class="w-5 h-5 text-muted-foreground lg:w-8 lg:h-8" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-2 0c0 .993-.241 1.929-.668 2.754l-1.524-1.525a3.997 3.997 0 00.078-2.183l1.562-1.562C15.802 8.249 16 9.1 16 10zm-5.165 3.913l1.58 1.58A5.98 5.98 0 0110 16a5.976 5.976 0 01-2.516-.552l1.562-1.562a4.006 4.006 0 001.789.027zm-4.677-2.796a4.002 4.002 0 01-.041-2.08l-.08.08-1.53-1.533A5.98 5.98 0 004 10c0 .954.223 1.856.619 2.657l1.54-1.54zm1.088-6.45A5.974 5.974 0 0110 4c.954 0 1.856.223 2.657.619l-1.54 1.54a4.002 4.002 0 00-2.346.033L7.246 4.668zM12 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<p class="mb-2 text-xl font-bold text-foreground">{content.contact_support.title}</p>
<p class="mb-3 text-muted-foreground">{content.contact_support.description}</p>
<a
href={content.contact_support.href}
class="inline-flex py-2 px-4 text-sm font-medium rounded-lg border border-primary text-primary hover:bg-primary hover:text-primary-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring"
>
{content.contact_support.label}
</a>
</div>
<div>
<div class="flex justify-center items-center mx-auto mb-4 w-10 h-10 bg-muted rounded-lg lg:h-16 lg:w-16">
<svg class="w-5 h-5 text-muted-foreground lg:w-8 lg:h-8" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" />
</svg>
</div>
<p class="mb-2 text-xl font-bold text-foreground">{content.contact_address.title}</p>
<p class="mb-3 text-muted-foreground">{content.contact_address.description}</p>
<a
href="https://maps.google.com/?q={encodeURIComponent(content.contact_address.address)}"
target="_blank"
rel="noopener noreferrer"
class="font-semibold text-primary hover:underline"
>
{content.contact_address.address}
</a>
</div>
</div>
</div>
</section>