Contact Form HTML CSS JS #3

This contact form HTML CSS JS tutorial shows you how to build a two-column contact section with a dark left panel and a white form panel on the right. The left side has a bold display heading, contact details with Bootstrap Icons, and office hours.

The right side has a clean form with inline validation that highlights fields in red and shows error messages when required fields are empty or incorrectly filled. The submit button uses a neo-brutalist offset shadow style that animates on hover.

Final output:

Let’s
Talk.

Fill out the form and we’ll get back to you with a direct, no-nonsense reply. No bots. No templates.

Office

28 W 23rd St, New York, NY 10010

Hours Mon – Fri, 9:00 AM – 6:00 PM ET
Name is required.
Email is required.
Message is required.

Your information is never shared with third parties.


Step-by-step guide for Contact Form HTML CSS JS #3:

1. Link the Work Sans and Archivo fonts from Google Fonts and Bootstrap Icons in the head.

<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=Archivo:ital,wght@0,100..900;1,100..900&family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap"
    rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">

Let’s start with a section element with the class contact-section. Inside it, add a .contact-wrapper div with a .contact-left and a .contact-right div.

<section class="contact-section">
    <div class="contact-wrapper">
        <div class="contact-left"></div>
        <div class="contact-right"></div>
    </div>
</section>

Then reset margins, define CSS variables for the yellow accent and near-black color on :root, and set Work Sans as the font family. Give the section a warm off-white background and some padding. Style .contact-wrapper with a two-column grid and a dark border. On mobile, collapse to a single column so both sides stack vertically.

* {
    margin: 0;
    box-sizing: border-box;
}
:root {
    --yellow: hsl(54, 100%, 50%);
    --black: hsl(0, 0%, 5%);
}
body {
    font-family: "Work Sans", sans-serif;
}
.contact-section {
    background-color: hsl(39, 35%, 92%);
    padding: 4rem 1rem;
}
.contact-section .contact-wrapper {
    max-width: 64rem;
    margin-inline: auto;
    border: 2px solid var(--black);
    display: grid;
    grid-template-columns: 1fr 1.5fr;
}
@media screen and (max-width: 576px) {
    .contact-section .contact-wrapper {
        grid-template-columns: 1fr;
    }
}

2. Fill in .contact-left with the heading, description, contact details, and hours.

<div class="contact-left">
    <h2>Let's <br> Talk.</h2>
    <p>Fill out the form and we'll get back to you with a direct, no-nonsense reply. No bots. No templates.</p>
    <div class="contact-details-wrapper">
        <div class="contact-detail">
            <i class="bi bi-envelope"></i>
            <div>
                <span>Email</span>
                <a href="mailto:hello@studio.works">hello@studio.works</a>
            </div>
        </div>
        <div class="contact-detail">
            <i class="bi bi-telephone"></i>
            <div>
                <span>Phone</span>
                <a href="tel:+1 (212) 555-0174">+1 (212) 555-0174</a>
            </div>
        </div>
        <div class="contact-detail">
            <i class="bi bi-geo-alt"></i>
            <div>
                <span>Office</span>
                <p>28 W 23rd St, New York, NY 10010</p>
            </div>
        </div>
    </div>
    <div class="hours">
        <span>Hours</span>
        <span>Mon - Fri, 9:00 AM - 6:00 PM ET</span>
    </div>
</div>

Give .contact-left a near-black background and use clamp() on the padding so it scales with the screen size. Use the Archivo font for the heading in yellow since it’s the most important element on the left side. Set a slight opacity reduction on everything except the heading so the heading stands out as the primary focus.

.contact-section .contact-left {
    background-color: var(--black);
    padding: clamp(1.25rem, calc(1rem + 1.67vw), 2.5rem);
}
.contact-section .contact-left *:not(h2) {
    opacity: .9;
}
.contact-section .contact-left h2 {
    font-family: "Archivo", sans-serif;
    font-weight: 600;
    font-size: clamp(2.5rem, calc(1rem + 3.4vw), 3.75rem);
    line-height: 1;
    letter-spacing: -0.025em;
    text-transform: uppercase;
    color: var(--yellow);
    margin-bottom: 2rem;
}
.contact-section .contact-left > p {
    font-size: .875rem;
    line-height: 1.5;
    margin-bottom: 2.5rem;
    color: var(--yellow);
    opacity: .8;
    max-width: 20rem;
}

Output:

Let’s
Talk.

Fill out the form and we’ll get back to you with a direct, no-nonsense reply. No bots. No templates.

Office

28 W 23rd St, New York, NY 10010

Hours Mon – Fri, 9:00 AM – 6:00 PM ET

3. Style the contact details. Use flexbox with the flex direction property set to column on .contact-details-wrapper to stack the three detail rows. Add a bottom border and some margin to separate the details from the hours below. Use flexbox on each .contact-detail to lay the icon and text side by side. Style the label with uppercase text and a wide letter spacing, and the link as a block element below it.

.contact-section .contact-details-wrapper {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    margin-bottom: 3rem;
    border-bottom: 1px solid hsla(54, 100%, 50%, 0.2);
}
.contact-section .contact-detail {
    display: flex;
    gap: .75rem;
    color: var(--yellow);
}
.contact-section .contact-detail i {
    font-size: .875rem;
    margin-top: 0.125rem;
    opacity: .6;
}
.contact-section .contact-detail div span {
    font-size: .75rem;
    line-height: 1.3;
    letter-spacing: 1.8px;
    text-transform: uppercase;
    opacity: .6;
}
.contact-section .contact-detail div a,
.contact-section .contact-detail div p {
    font-size: .875rem;
    line-height: 1.4;
    display: block;
    margin-top: 0.125rem;
    color: var(--yellow);
    text-decoration: none;
}

Style the hours with the same label pattern as the contact details, since both use the same small uppercase font with wide letter spacing.

.contact-section .hours {
    color: var(--yellow);
    opacity: .6;
}
.contact-section .hours span:first-of-type {
    font-size: .75rem;
    line-height: 1.3;
    letter-spacing: 1.8px;
    text-transform: uppercase;
    margin-bottom: .75rem;
    display: block;
    opacity: .6;
}

Output:

Let’s
Talk.

Fill out the form and we’ll get back to you with a direct, no-nonsense reply. No bots. No templates.

Office

28 W 23rd St, New York, NY 10010

Hours Mon – Fri, 9:00 AM – 6:00 PM ET

4. Fill in .contact-right with the contact form. Use a form element with the class contact-form. Each row of fields goes inside a .form-row div, and each individual field goes inside a .form-field div with a label, an input or textarea, and an .error-message span.

Adding the novalidate attribute to the form disables the browser’s default validation popups, since we’ll handle validation ourselves in the JavaScript later.

<div class="contact-right">
    <form action="#" method="get" class="contact-form" novalidate>
        <div class="form-row">
            <div class="form-field">
                <label for="name">Full name</label>
                <input type="text" name="name" id="name" required placeholder="Jordan Lee" />
                <span class="error-message">Name is required.</span>
            </div>
            <div class="form-field">
                <label for="email">Email address</label>
                <input type="email" name="email" id="email" required placeholder="jordan@example.com" />
                <span class="error-message">Email is required.</span>
            </div>
        </div>
        <div class="form-row">
            <div class="form-field">
                <label for="subject">Subject</label>
                <input type="text" name="subject" id="subject" placeholder="Select a subject..." />
            </div>
        </div>
        <div class="form-row">
            <div class="form-field">
                <label for="message">Message</label>
                <textarea name="message" rows="5" id="message" required placeholder="Tell us what you're working on..."></textarea>
                <span class="error-message">Message is required.</span>
            </div>
        </div>
        <div class="form-row">
            <button type="submit" class="contact-submit-btn">
                <span>Send Message</span>
                <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M3.75 9H14.25" stroke="#0D0D0D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
                    <path d="M9 3.75L14.25 9L9 14.25" stroke="#0D0D0D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
                </svg>
            </button>
        </div>
        <p>Your information is never shared with third parties.</p>
    </form>
</div>

Give .contact-right a white background and the same scaled padding as the left side. Style each .form-row with flexbox and a bottom margin to space the rows out. On mobile, switch the flex direction to column so side-by-side fields stack vertically.

.contact-section .contact-right {
    padding: clamp(1.25rem, calc(1rem + 1.67vw), 2.5rem);
    background-color: hsl(0, 0%, 100%);
}
.contact-section .form-row {
    display: flex;
    gap: 1.5rem;
    margin-bottom: 1.5rem;
}
@media screen and (max-width: 576px) {
    .contact-section .form-row {
        flex-direction: column;
    }
}

Output:

Let’s
Talk.

Fill out the form and we’ll get back to you with a direct, no-nonsense reply. No bots. No templates.

Office

28 W 23rd St, New York, NY 10010

Hours Mon – Fri, 9:00 AM – 6:00 PM ET
Name is required.
Email is required.
Message is required.

Your information is never shared with third parties.

5. Style the form fields. Give the label a small uppercase font with wide letter spacing. Style the input and textarea with a full dark border and a subtle yellow background on focus so the user knows which field is active. Set the width property to 100% so each field fills its row.

.contact-section .form-field {
    width: 100%;
}
.contact-section .form-field label {
    font-weight: 600;
    font-size: .75rem;
    line-height: 1.3;
    letter-spacing: 1.8px;
    text-transform: uppercase;
    color: var(--black);
}
.contact-section .form-field input,
.contact-section .form-field textarea {
    font-size: 1rem;
    width: 100%;
    min-height: 3rem;
    background: none;
    border: 2px solid var(--black);
    margin-top: .375rem;
    padding: .875rem 1rem;
    font-family: "Work Sans", sans-serif;
    font-weight: 400;
    font-size: .875rem;
    line-height: 1.4;
    transition: background-color .3s ease;
}
.contact-section .form-field input:focus,
.contact-section .form-field textarea:focus {
    background-color: hsla(54, 100%, 50%, .1);
    outline: none;
}

Output:

Let’s
Talk.

Fill out the form and we’ll get back to you with a direct, no-nonsense reply. No bots. No templates.

Office

28 W 23rd St, New York, NY 10010

Hours Mon – Fri, 9:00 AM – 6:00 PM ET
Name is required.
Email is required.
Message is required.

Your information is never shared with third parties.

6. Style the error states. When a .form-field gets the has-error class added by JavaScript, change the border to red and make the .error-message span visible. By default, the error message is hidden, so it only shows up when validation fails.

.contact-section .form-field.has-error input,
.contact-section .form-field.has-error textarea {
    border-color: hsl(0, 84%, 55%);
}
.contact-section .error-message {
    display: none;
    color: hsl(0, 100%, 62%);
    margin-top: .375rem;
    font-weight: 600;
    font-size: .75rem;
    line-height: 1.3;
}
.contact-section .form-field.has-error .error-message {
    display: block;
}

Now style the submit button with the yellow background and a dark border. Add a box-shadow offset to the bottom right to give it a pressed look. On hover, remove the shadow and translate the button down slightly so it feels like it’s being pushed in.

.contact-section .contact-submit-btn {
    display: flex;
    justify-content: space-between;
    gap: .5rem;
    font-weight: 600;
    font-size: .875rem;
    line-height: 1.4;
    letter-spacing: 1.68px;
    text-transform: uppercase;
    padding: 1rem 1.75rem;
    background-color: var(--yellow);
    width: 100%;
    border: 2px solid var(--black);
    box-shadow: 4px 4px 0px 0px var(--black);
    transition: transform .3s, box-shadow .3s;
    cursor: pointer;
}
.contact-section .contact-submit-btn:hover {
    box-shadow: none;
    transform: translateY(.25rem);
}
.contact-section .contact-form > p {
    font-weight: 400;
    font-size: .75rem;
    line-height: 1.3;
    text-align: center;
    color: hsl(0, 0%, 33%);
}

Output:

Let’s
Talk.

Fill out the form and we’ll get back to you with a direct, no-nonsense reply. No bots. No templates.

Office

28 W 23rd St, New York, NY 10010

Hours Mon – Fri, 9:00 AM – 6:00 PM ET
Name is required.
Email is required.
Message is required.

Your information is never shared with third parties.

7. Finally, let’s add the form validation JavaScript. Link script.js before the closing body tag. When the form is submitted, prevent the default browser behaviour using e.preventDefault() so the page doesn’t reload.

Then loop through every required field and check its validity state. If the field is empty, add the has-error class to show the error message. And if the email format is wrong, update the error message text and add has-error. If the field is valid, remove has-error so the error message disappears.

document.addEventListener("DOMContentLoaded", () => {
    document.querySelector('.contact-form').addEventListener('submit', function (e) {
        e.preventDefault();
        this.querySelectorAll('[required]').forEach(field => {
            const wrapper = field.closest('.form-field');
            const errorMsg = wrapper.querySelector('.error-message');

            if (field.validity.valueMissing) {
                errorMsg.textContent = field.name === 'email' ? 'Email is required.' : errorMsg.textContent;
                wrapper.classList.add('has-error');
            } else if (field.validity.typeMismatch) {
                errorMsg.textContent = 'Please enter a valid email address.';
                wrapper.classList.add('has-error');
            } else {
                wrapper.classList.remove('has-error');
            }
        });
    });
});

Output:

Let’s
Talk.

Fill out the form and we’ll get back to you with a direct, no-nonsense reply. No bots. No templates.

Office

28 W 23rd St, New York, NY 10010

Hours Mon – Fri, 9:00 AM – 6:00 PM ET
Name is required.
Email is required.
Message is required.

Your information is never shared with third parties.


Final Output Code for Responsive Contact Form HTML CSS JS #3:

HTML

<section class="contact-section">
    <div class="contact-wrapper">
        <div class="contact-left">
            <h2>Let's <br> Talk.</h2>
            <p>Fill out the form and we'll get back to you
                with a direct, no-nonsense reply. No bots. No
                templates.</p>
            <div class="contact-details-wrapper">
                <div class="contact-detail">
                    <i class="bi bi-envelope"></i>
                    <div>
                        <span>Email</span>
                        <a href="mailto:hello@studio.works">hello@studio.works</a>
                    </div>
                </div>
                <div class="contact-detail">
                    <i class="bi bi-telephone"></i>
                    <div>
                        <span>Phone</span>
                        <a href="tel:+1 (212) 555-0174">+1 (212) 555-0174</a>
                    </div>
                </div>
                <div class="contact-detail">
                    <i class="bi bi-geo-alt"></i>
                    <div>
                        <span>Office</span>
                        <p>28 W 23rd St, New York, NY 10010</p>
                    </div>
                </div>
            </div>
            <div class="hours">
                <span>Hours</span>
                <span>Mon - Fri, 9:00 AM - 6:00 PM ET</span>
            </div>
        </div>
        <div class="contact-right">
            <form action="#" method="get" class="contact-form" novalidate>
                <div class="form-row">
                    <div class="form-field">
                        <label for="name">Full name</label>
                        <input type="text" name="name" id="name" required placeholder="Jordan Lee" />
                        <span class="error-message">Name is required.</span>
                    </div>
                    <div class="form-field">
                        <label for="email">Email address</label>
                        <input type="email" name="email" id="email" required placeholder="jordan@example.com" />
                        <span class="error-message" id="email-error">Email is required.</span>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-field">
                        <label for="subject">Subject</label>
                        <input type="text" name="subject" id="subject" placeholder="Select a subject..." />
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-field">
                        <label for="message">Message</label>
                        <textarea name="message" rows="5" id="message" required
                            placeholder="Tell us what you're working on, what you need, and any relevant context..."></textarea>
                        <span class="error-message">Message is required.</span>
                    </div>
                </div>
                <div class="form-row">
                    <button type="submit" class="contact-submit-btn">
                        <span>Send Message</span>
                        <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
                            <path d="M3.75 9H14.25" stroke="#0D0D0D" stroke-width="1.5" stroke-linecap="round"
                                stroke-linejoin="round" />
                            <path d="M9 3.75L14.25 9L9 14.25" stroke="#0D0D0D" stroke-width="1.5" stroke-linecap="round"
                                stroke-linejoin="round" />
                        </svg>
                    </button>
                </div>
                <p>Your information is never shared with third parties.</p>
            </form>
        </div>
    </div>
</section>

CSS

* {
    margin: 0;
    box-sizing: border-box;
}
:root {
    --yellow: hsl(54, 100%, 50%);
    --black: hsl(0, 0%, 5%);
}
body {
    font-family: "Work Sans", sans-serif;
}
.contact-section {
    background-color: hsl(39, 35%, 92%);
    padding: 4rem 1rem;
}
.contact-section .contact-wrapper {
    max-width: 64rem;
    margin-inline: auto;
    border: 2px solid var(--black);
    display: grid;
    grid-template-columns: 1fr 1.5fr;
}
@media screen and (max-width: 576px) {
    .contact-section .contact-wrapper {
        grid-template-columns: 1fr;
    }
}
.contact-section .contact-left {
    background-color: var(--black);
    padding: clamp(1.25rem, calc(1rem + 1.67vw), 2.5rem);
}
.contact-section .contact-left *:not(h2) {
    opacity: .9;
}
.contact-section .contact-left h2 {
    font-family: "Archivo", sans-serif;
    font-weight: 600;
    font-size: clamp(2.5rem, calc(1rem + 3.4vw), 3.75rem);
    line-height: 1;
    letter-spacing: -0.025em;
    text-transform: uppercase;
    color: var(--yellow);
    margin-bottom: 2rem;
}
.contact-section .contact-left > p {
    font-size: .875rem;
    line-height: 1.5;
    margin-bottom: 2.5rem;
    color: var(--yellow);
    opacity: .8;
    max-width: 20rem;
}
.contact-section .contact-details-wrapper {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    margin-bottom: 3rem;
    border-bottom: 1px solid hsla(54, 100%, 50%, 0.2);
}
.contact-section .contact-detail {
    display: flex;
    gap: .75rem;
    color: var(--yellow);
}
.contact-section .contact-detail i {
    font-size: .875rem;
    margin-top: 0.125rem;
    opacity: .6;
}
.contact-section .contact-detail div span {
    font-size: .75rem;
    line-height: 1.3;
    letter-spacing: 1.8px;
    text-transform: uppercase;
    opacity: .6;
}
.contact-section .contact-detail div a,
.contact-section .contact-detail div p {
    font-size: .875rem;
    line-height: 1.4;
    display: block;
    margin-top: 0.125rem;
    color: var(--yellow);
    text-decoration: none;
}
.contact-section .hours {
    color: var(--yellow);
    opacity: .6;
}
.contact-section .hours span:first-of-type {
    font-size: .75rem;
    line-height: 1.3;
    letter-spacing: 1.8px;
    text-transform: uppercase;
    margin-bottom: .75rem;
    display: block;
    opacity: .6;
}
.contact-section .contact-right {
    padding: clamp(1.25rem, calc(1rem + 1.67vw), 2.5rem);
    background-color: hsl(0, 0%, 100%);
}
.contact-section .form-row {
    display: flex;
    gap: 1.5rem;
    margin-bottom: 1.5rem;
}
@media screen and (max-width: 576px) {
    .contact-section .form-row {
        flex-direction: column;
    }
}
.contact-section .form-field {
    width: 100%;
}
.contact-section .form-field label {
    font-weight: 600;
    font-size: .75rem;
    line-height: 1.3;
    letter-spacing: 1.8px;
    text-transform: uppercase;
    color: var(--black);
}
.contact-section .form-field input,
.contact-section .form-field textarea {
    width: 100%;
    min-height: 3rem;
    background: none;
    border: 2px solid var(--black);
    margin-top: .375rem;
    padding: .875rem 1rem;
    font-family: "Work Sans", sans-serif;
    font-weight: 400;
    font-size: .875rem;
    line-height: 1.4;
    transition: background-color .3s ease;
}
.contact-section .form-field input:focus,
.contact-section .form-field textarea:focus {
    background-color: hsla(54, 100%, 50%, .1);
    outline: none;
}
.contact-section .form-field.has-error input,
.contact-section .form-field.has-error textarea {
    border-color: hsl(0, 84%, 55%);
}
.contact-section .error-message {
    display: none;
    color: hsl(0, 100%, 62%);
    margin-top: .375rem;
    font-weight: 600;
    font-size: .75rem;
    line-height: 1.3;
}
.contact-section .form-field.has-error .error-message {
    display: block;
}
.contact-section .contact-submit-btn {
    display: flex;
    justify-content: space-between;
    gap: .5rem;
    font-weight: 600;
    font-size: .875rem;
    line-height: 1.4;
    letter-spacing: 1.68px;
    text-transform: uppercase;
    padding: 1rem 1.75rem;
    background-color: var(--yellow);
    width: 100%;
    border: 2px solid var(--black);
    box-shadow: 4px 4px 0px 0px var(--black);
    transition: transform .3s, box-shadow .3s;
    cursor: pointer;
}
.contact-section .contact-submit-btn:hover {
    box-shadow: none;
    transform: translateY(.25rem);
}
.contact-section .contact-form > p {
    font-weight: 400;
    font-size: .75rem;
    line-height: 1.3;
    text-align: center;
    color: hsl(0, 0%, 33%);
}

JS

document.addEventListener("DOMContentLoaded", () => {
    document.querySelector('.contact-form').addEventListener('submit', function (e) {
        e.preventDefault();
        this.querySelectorAll('[required]').forEach(field => {
            const wrapper = field.closest('.form-field');
            const errorMsg = wrapper.querySelector('.error-message');

            if (field.validity.valueMissing) {
                errorMsg.textContent = field.name === 'email' ? 'Email is required.' : errorMsg.textContent;
                wrapper.classList.add('has-error');
            } else if (field.validity.typeMismatch) {
                errorMsg.textContent = 'Please enter a valid email address.';
                wrapper.classList.add('has-error');
            } else {
                wrapper.classList.remove('has-error');
            }
        });
    });
});

If you have any doubts or stuck somewhere, you can reach out through Coding Yaar's Discord server.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Most Voted
Newest Oldest
0
Would love your thoughts, please comment.x
()
x