DEV Community

Cover image for Build a Modern Contact Us Page with Next.js + Tailwind CSS (Component-Based + SEO-Ready)
Mina Golzari Dalir
Mina Golzari Dalir

Posted on

Build a Modern Contact Us Page with Next.js + Tailwind CSS (Component-Based + SEO-Ready)

A well-structured contact page is a must-have for any website. In this tutorial we’ll:

  1. Create reusable React components (ContactForm, ContactInfo, SocialLinks, MapEmbed).
  2. Compose the page in the Next.js App Router (or Pages Router).
  3. Add full SEO – title, meta description, Open Graph, Twitter cards, canonical URL, and optional JSON-LD.
app/
contact/
page.tsx          ← main page (App Router)
components/
ContactForm.tsx
ContactInfo.tsx
SocialLinks.tsx
MapEmbed.tsx
public/
og-contact.jpg      ← 1200×630 Open Graph image
Enter fullscreen mode Exit fullscreen mode

1. Install Dependencies

npm install lucide-react
# for Pages Router SEO
npm install next-seo
Enter fullscreen mode Exit fullscreen mode

2. Reusable Components
components/ContactForm.tsx

'use client';

import { useState } from 'react';

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    subject: '',
    message: '',
  });

  const [status, setStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle');

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setStatus('sending');

    // Replace with real API (Formspree, EmailJS, etc.)
    setTimeout(() => {
      setStatus('success');
      setFormData({ name: '', email: '', subject: '', message: '' });
      setTimeout(() => setStatus('idle'), 3000);
    }, 1000);
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <div>
          <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
            Full Name
          </label>
          <input
            type="text"
            id="name"
            name="name"
            required
            value={formData.name}
            onChange={handleChange}
            className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
            placeholder="John Doe"
          />
        </div>

        <div>
          <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
            Email Address
          </label>
          <input
            type="email"
            id="email"
            name="email"
            required
            value={formData.email}
            onChange={handleChange}
            className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
            placeholder="john@example.com"
          />
        </div>
      </div>

      <div>
        <label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1">
          Subject
        </label>
        <input
          type="text"
          id="subject"
          name="subject"
          required
          value={formData.subject}
          onChange={handleChange}
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
          placeholder="How can we help you?"
        />
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          required
          rows={5}
          value={formData.message}
          onChange={handleChange}
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition resize-none"
          placeholder="Tell us more..."
        />
      </div>

      <button
        type="submit"
        disabled={status === 'sending'}
        className={`w-full py-3 px-6 text-white font-medium rounded-lg transition ${
          status === 'sending'
            ? 'bg-gray-400 cursor-not-allowed'
            : 'bg-blue-600 hover:bg-blue-700'
        }`}
      >
        {status === 'sending' ? 'Sending...' : 'Send Message'}
      </button>

      {status === 'success' && (
        <p className="text-green-600 text-center font-medium">
          Message sent successfully!
        </p>
      )}
      {status === 'error' && (
        <p className="text-red-600 text-center font-medium">
          Failed to send. Try again.
        </p>
      )}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

components/ContactInfo.tsx

import { Mail, Phone, MapPin } from 'lucide-react';

export default function ContactInfo() {
  return (
    <div className="space-y-6">
      <h3 className="text-xl font-semibold text-gray-900">Get in Touch</h3>
      <p className="text-gray-600">
        We'd love to hear from you. Send us a message and we'll respond as soon as possible.
      </p>

      <div className="space-y-4">
        <div className="flex items-start gap-3">
          <MapPin className="w-5 h-5 text-blue-600 mt-0.5" />
          <div>
            <p className="font-medium text-gray-900">Address</p>
            <p className="text-gray-600">
              123 Business St, Suite 100<br />New York, NY 10001
            </p>
          </div>
        </div>

        <div className="flex items-start gap-3">
          <Phone className="w-5 h-5 text-blue-600 mt-0.5" />
          <div>
            <p className="font-medium text-gray-900">Phone</p>
            <p className="text-gray-600">+1 (555) 123-4567</p>
          </div>
        </div>

        <div className="flex items-start gap-3">
          <Mail className="w-5 h-5 text-blue-600 mt-0.5" />
          <div>
            <p className="font-medium text-gray-900">Email</p>
            <p className="text-gray-600">hello@company.com</p>
          </div>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

components/SocialLinks.tsx

import { Facebook, Twitter, Linkedin, Instagram } from 'lucide-react';

export default function SocialLinks() {
  const socials = [
    { Icon: Facebook, label: 'Facebook', href: '#' },
    { Icon: Twitter, label: 'Twitter', href: '#' },
    { Icon: Linkedin, label: 'LinkedIn', href: '#' },
    { Icon: Instagram, label: 'Instagram', href: '#' },
  ];

  return (
    <div className="flex gap-4">
      {socials.map(({ Icon, label, href }) => (
        <a
          key={label}
          href={href}
          aria-label={label}
          className="p-3 bg-gray-100 rounded-full hover:bg-blue-600 hover:text-white transition group"
        >
          <Icon className="w-5 h-5 text-gray-700 group-hover:text-white" />
        </a>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

components/MapEmbed.tsx

export default function MapEmbed() {
  return (
    <div className="w-full h-64 md:h-80 rounded-lg overflow-hidden shadow-lg">
      <iframe
        src="https://www.google.com/maps/embed?pb=..."
        width="100%"
        height="100%"
        style={{ border: 0 }}
        allowFullScreen
        loading="lazy"
        referrerPolicy="no-referrer-when-downgrade"
        title="Company Location"
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tip: Generate the embed URL from Google Maps → Share → Embed a map.

3. The Contact Page (App Router)
app/contact/page.tsx

import ContactForm from '@/components/ContactForm';
import ContactInfo from '@/components/ContactInfo';
import SocialLinks from '@/components/SocialLinks';
import MapEmbed from '@/components/MapEmbed';

// ──────── SEO METADATA ────────
export const metadata = {
  title: 'Contact Us | Your Company Name',
  description:
    'Get in touch with us! Send a message, call, or visit our office. We respond within 24 hours.',
  keywords:
    'contact, support, reach out, customer service, Your Company Name',
  openGraph: {
    title: 'Contact Us | Your Company Name',
    description:
      'We’d love to hear from you. Send us a message and we’ll respond as soon as possible.',
    url: 'https://www.yourwebsite.com/contact',
    siteName: 'Your Company Name',
    images: [
      {
        url: 'https://www.yourwebsite.com/og-contact.jpg',
        width: 1200,
        height: 630,
        alt: 'Contact Us – Your Company Name',
      },
    ],
    locale: 'en_US',
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Contact Us | Your Company Name',
    description: 'Get in touch with us anytime!',
    images: ['https://www.yourwebsite.com/og-contact.jpg'],
  },
  robots: { index: true, follow: true },
  alternates: { canonical: 'https://www.yourwebsite.com/contact' },
};

export default function ContactPage() {
  return (
    <div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-7xl mx-auto">
        {/* Hero */}
        <div className="text-center mb-12">
          <h1 className="text-4xl font-bold text-gray-900 mb-4">Contact Us</h1>
          <p className="text-lg text-gray-600 max-w-2xl mx-auto">
            Have a question or want to work together? Fill out the form below and we'll get back to you.
          </p>
        </div>

        {/* Layout */}
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
          {/* Left column */}
          <div className="lg:col-span-1 space-y-8">
            <ContactInfo />
            <div>
              <h4 className="text-lg font-semibold text-gray-900 mb-4">Follow Us</h4>
              <SocialLinks />
            </div>
          </div>

          {/* Right column – Form */}
          <div className="lg:col-span-2 bg-white p-8 rounded-xl shadow-md">
            <ContactForm />
          </div>
        </div>

        {/* Optional Map */}
        <div className="mt-12">
          <MapEmbed />
        </div>

        {/* ──────── OPTIONAL JSON-LD ──────── */}
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify({
              '@context': 'https://schema.org',
              '@type': 'ContactPage',
              name: 'Contact Us - Your Company Name',
              url: 'https://www.yourwebsite.com/contact',
              description:
                'Contact form and information for Your Company Name',
              publisher: {
                '@type': 'Organization',
                name: 'Your Company Name',
                logo: {
                  '@type': 'ImageObject',
                  url: 'https://www.yourwebsite.com/logo.png',
                },
              },
            }),
          }}
        />
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Pages-Router Alternative (if you’re not on App Router)

// pages/contact.tsx
import Head from 'next/head';
import { NextSeo } from 'next-seo';
import ContactForm from '@/components/ContactForm';
import ContactInfo from '@/components/ContactInfo';
import SocialLinks from '@/components/SocialLinks';
import MapEmbed from '@/components/MapEmbed';

export default function ContactPage() {
  const SEO = { /* same object as metadata above */ };

  return (
    <>
      <NextSeo {...SEO} />
      <Head>
        <meta name="keywords" content="contact, support, ..." />
        <meta name="robots" content="index, follow" />
      </Head>

      {/* ...same JSX as inside the App Router page... */}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Open Graph Image
Create public/og-contact.jpg

Dimensions: 1200 × 630 px
Content: “Contact Us” + logo + subtle background
Tools: Canva, Figma, or Photoshop

That’s it! You now have a clean, component-based, fully SEO-optimized Contact Us page ready for production.

Top comments (0)