Contact page with real-time form validation and animated toast notifications ✨ perfect for modern web projects
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bnaw — Contact</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap');
:root{
--ink:#151316;
--paper:#f0ede6;
--card:#ffffff;
--clay:#b5502e;
--moss:#4a5d43;
--red:#c2402c;
--muted:#6b675f;
--line:#e3ddd0;
}
*{box-sizing:border-box; margin:0; padding:0;}
body{
background:var(--paper);
color:var(--ink);
font-family:'Space Grotesk', sans-serif;
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:32px 16px;
}
.wrap{
width:100%;
max-width:920px;
background:var(--card);
border-radius:20px;
box-shadow:0 24px 60px -30px rgba(21,19,22,0.25);
overflow:hidden;
position:relative;
}
.top{
display:flex;
justify-content:space-between;
align-items:flex-start;
padding:30px 36px;
border-bottom:1px solid var(--line);
gap:16px;
flex-wrap:wrap;
}
.logo{ font-size:26px; font-weight:700; letter-spacing:-0.02em; }
.logo span{ color:var(--clay); }
.tag{
font-family:'DM Mono', monospace;
font-size:12px;
color:var(--muted);
display:flex;
align-items:center;
gap:8px;
}
.dot{
width:7px; height:7px; border-radius:50%;
background:var(--moss);
animation:pulse 2s infinite;
}
@keyframes pulse{
0%,100%{ opacity:1; }
50%{ opacity:0.35; }
}
.grid{
display:grid;
grid-template-columns:1fr 1.15fr;
}
@media (max-width:720px){
.grid{ grid-template-columns:1fr; }
.info{ border-right:none !important; border-bottom:1px solid var(--line); }
}
.info{ padding:36px; border-right:1px solid var(--line); }
.info h1{ font-size:26px; line-height:1.2; letter-spacing:-0.01em; margin-bottom:14px; }
.info p.lead{ color:var(--muted); font-size:14px; line-height:1.6; margin-bottom:28px; max-width:34ch; }
.channel{
font-family:'DM Mono', monospace;
font-size:13px;
padding:14px 0;
border-top:1px solid var(--line);
display:flex;
justify-content:space-between;
align-items:center;
gap:10px;
transition:padding-left 0.15s ease;
}
.channel:last-child{ border-bottom:1px solid var(--line); }
.channel:hover{ padding-left:4px; }
.channel .label{ color:var(--muted); text-transform:uppercase; letter-spacing:0.06em; font-size:11px; }
.channel a{ color:var(--ink); text-decoration:none; }
.channel a:hover{ color:var(--clay); }
.form-side{ padding:36px; }
.field{ margin-bottom:20px; position:relative; }
.field label{
display:flex;
justify-content:space-between;
font-family:'DM Mono', monospace;
font-size:11px;
text-transform:uppercase;
letter-spacing:0.06em;
color:var(--muted);
margin-bottom:8px;
}
.field .err-msg{
color:var(--red);
font-size:11px;
text-transform:none;
letter-spacing:0;
opacity:0;
transform:translateY(-2px);
transition:opacity 0.15s ease, transform 0.15s ease;
}
.field.error .err-msg{ opacity:1; transform:translateY(0); }
.field input, .field textarea{
width:100%;
border:none;
border-bottom:2px solid var(--line);
background:transparent;
font-family:'Space Grotesk', sans-serif;
font-size:16px;
padding:8px 26px 8px 2px;
color:var(--ink);
outline:none;
resize:none;
transition:border-color 0.15s ease;
}
.field input:focus, .field textarea:focus{ border-bottom-color:var(--clay); }
.field.error input, .field.error textarea{ border-bottom-color:var(--red); }
.field.valid input{ border-bottom-color:var(--moss); }
.field textarea{ min-height:96px; }
.check-icon{
position:absolute;
right:2px;
top:34px;
font-size:14px;
color:var(--moss);
opacity:0;
transition:opacity 0.15s ease;
}
.field.valid .check-icon{ opacity:1; }
.submit{
margin-top:6px;
width:100%;
padding:15px 20px;
background:var(--ink);
color:var(--paper);
border:2px solid var(--ink);
font-family:'Space Grotesk', sans-serif;
font-weight:600;
font-size:14px;
text-transform:uppercase;
letter-spacing:0.05em;
cursor:pointer;
border-radius:10px;
transition:background 0.15s ease, color 0.15s ease, transform 0.08s ease;
}
.submit:hover{ background:var(--clay); border-color:var(--clay); color:#fff; }
.submit:active{ transform:scale(0.98); }
.submit.loading{ opacity:0.7; pointer-events:none; }
.foot{
border-top:1px solid var(--line);
padding:16px 36px;
font-family:'DM Mono', monospace;
font-size:11px;
color:var(--muted);
display:flex;
justify-content:space-between;
flex-wrap:wrap;
gap:8px;
}
.toast{
position:absolute;
top:20px;
left:50%;
transform:translate(-50%, -20px);
background:var(--ink);
color:var(--paper);
font-size:13px;
font-family:'DM Mono', monospace;
padding:12px 20px;
border-radius:10px;
display:flex;
align-items:center;
gap:10px;
opacity:0;
pointer-events:none;
transition:opacity 0.25s ease, transform 0.25s ease;
z-index:10;
box-shadow:0 12px 30px -10px rgba(0,0,0,0.4);
white-space:nowrap;
}
.toast.show{
opacity:1;
transform:translate(-50%, 0);
}
.toast .tick{
width:16px; height:16px;
border-radius:50%;
background:var(--moss);
display:flex; align-items:center; justify-content:center;
font-size:10px;
flex-shrink:0;
}
</style>
</head>
<body>
<div class="wrap">
<div class="toast" id="toast">
<span class="tick">✓</span>
<span id="toastMsg">Message sent</span>
</div>
<div class="top">
<div class="logo">bn<span>aw</span>.</div>
<div class="tag"><span class="dot"></span> Usually replies within a day</div>
</div>
<div class="grid">
<div class="info">
<h1>Let's talk.</h1>
<p class="lead">Questions, ideas, or just want to say hi — drop a line and someone from the team will get back to you.</p>
<div class="channel"><span class="label">Email</span><a href="mailto:hello@bnaw.co">hello@bnaw.co</a></div>
<div class="channel"><span class="label">Phone</span><a href="tel:+10000000000">+1 (000) 000-0000</a></div>
<div class="channel"><span class="label">Studio</span><span>123 Placeholder St, Your City</span></div>
<div class="channel"><span class="label">Hours</span><span>Mon–Fri, 9am–6pm</span></div>
</div>
<div class="form-side">
<form id="contactForm" novalidate>
<div class="field" id="nameField">
<label for="name">Name <span class="err-msg" id="nameErr">Enter your name</span></label>
<input type="text" id="name" name="name">
<span class="check-icon">✓</span>
</div>
<div class="field" id="emailField">
<label for="email">Email <span class="err-msg" id="emailErr">Enter a valid email</span></label>
<input type="email" id="email" name="email">
<span class="check-icon">✓</span>
</div>
<div class="field" id="messageField">
<label for="message">Message <span class="err-msg" id="messageErr">Say a little more</span></label>
<textarea id="message" name="message"></textarea>
</div>
<button type="submit" class="submit" id="submitBtn">Send message</button>
</form>
</div>
</div>
<div class="foot">
<span>© 2026 bnaw.</span>
<span>Replace placeholder contact details before publishing.</span>
</div>
</div>
<script>
const form = document.getElementById('contactForm');
const nameField = document.getElementById('nameField');
const emailField = document.getElementById('emailField');
const messageField = document.getElementById('messageField');
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
const messageInput = document.getElementById('message');
const submitBtn = document.getElementById('submitBtn');
const toast = document.getElementById('toast');
const toastMsg = document.getElementById('toastMsg');
function validateField(field, input, test){
const ok = test(input.value.trim());
field.classList.toggle('valid', ok && input.value.trim() !== '');
return ok;
}
function showError(field){
field.classList.add('error');
field.classList.remove('valid');
}
function clearError(field){
field.classList.remove('error');
}
[nameInput, emailInput, messageInput].forEach((input, i) => {
const field = [nameField, emailField, messageField][i];
input.addEventListener('input', () => clearError(field));
input.addEventListener('blur', () => {
const value = input.value.trim();
if(!value) return;
if(input === emailInput){
const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
ok ? field.classList.add('valid') : showError(field);
} else {
field.classList.add('valid');
}
});
});
function showToast(message, isError){
toastMsg.textContent = message;
toast.querySelector('.tick').textContent = isError ? '!' : '✓';
toast.querySelector('.tick').style.background = isError ? 'var(--red)' : 'var(--moss)';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3200);
}
form.addEventListener('submit', function(e){
e.preventDefault();
let valid = true;
if(!nameInput.value.trim()){ showError(nameField); valid = false; } else { nameField.classList.add('valid'); }
const emailVal = emailInput.value.trim();
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)){ showError(emailField); valid = false; } else { emailField.classList.add('valid'); }
if(!messageInput.value.trim() || messageInput.value.trim().length < 5){ showError(messageField); valid = false; }
if(!valid){
showToast('Please fix the highlighted fields', true);
return;
}
submitBtn.classList.add('loading');
submitBtn.textContent = 'Sending…';
setTimeout(() => {
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Send message';
showToast('Message sent — we\'ll be in touch', false);
form.reset();
[nameField, emailField, messageField].forEach(f => f.classList.remove('valid', 'error'));
}, 900);
});
</script>
</body>
</html>