Modern contact forms don't have to feel boring. With HTML, CSS, and a little JavaScript, we can turn a standard form into a modern glass-style component with floating labels, smooth interactions, validation, and a success state.
In this tutorial, we'll look at the main ideas behind building a Glass Contact Form using HTML, CSS, and vanilla JavaScript.
What We're Building
The component starts with a simple Contact Us button.
When the user clicks it:
- The Contact Us button disappears
- A glass-style contact card opens
- Users can enter their name, email, and message
- Native browser validation checks the required fields
- A success message appears after a valid submission
- The form can be closed and reopened
- The Escape key can also close the panel
No framework or external JavaScript library is required.
HTML Structure
The component begins with a wrapper containing the trigger button and expandable contact panel.
<div class="cr-popup-contact">
<button
class="cr-popup-contact__trigger"
type="button"
aria-expanded="false"
>
Contact Us
<span>↗</span>
</button>
<div
class="cr-popup-contact__panel"
aria-hidden="true"
>
<div class="cr-popup-contact__card">
<button
class="cr-popup-contact__close"
type="button"
aria-label="Close contact form"
>
×
</button>
<div class="cr-popup-contact__content">
<span class="cr-popup-contact__eyebrow">
GET IN TOUCH
</span>
<h2>Let's start a conversation</h2>
<p>
Have a question or an idea?
Send us a message and we'll get back to you.
</p>
</div>
</div>
</div>
</div>
Keeping the trigger and panel inside the same component wrapper makes the component easier to reuse and keeps its behavior isolated.
Creating the Glass UI
The main contact card uses a dark layered background, subtle border, rounded corners, and deep shadows.
.cr-popup-contact__card {
position: relative;
width: min(100%, 460px);
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 22px;
color: #f8fafc;
background:
linear-gradient(
145deg,
#111827,
#070c1a
);
box-shadow:
0 28px 65px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
Cyan and purple blurred elements can then be positioned around the card to create the glowing glass appearance without relying on background images.
The result is a lightweight interface that still has plenty of visual depth.
Floating Form Labels
Instead of keeping labels permanently above each field, we can place them inside the inputs and move them when a field receives focus or contains a value.
.cr-popup-contact__field {
position: relative;
}
.cr-popup-contact__field label {
position: absolute;
top: 15px;
left: 14px;
color: #7f8ba0;
font-size: 13px;
pointer-events: none;
transition:
top 0.2s ease,
color 0.2s ease,
font-size 0.2s ease;
}
.cr-popup-contact__field input:focus + label,
.cr-popup-contact__field input:not(:placeholder-shown) + label,
.cr-popup-contact__field textarea:focus + label,
.cr-popup-contact__field textarea:not(:placeholder-shown) + label {
top: 6px;
color: #22d3ee;
font-size: 9px;
font-weight: 700;
}
The :placeholder-shown selector lets the label remain in its floating position after the user has entered a value.
Adding Focus Effects
A subtle cyan focus state makes it easier to see which field is currently active.
.cr-popup-contact__field input:focus,
.cr-popup-contact__field textarea:focus {
border-color: rgba(34, 211, 238, 0.68);
background: rgba(4, 12, 28, 0.82);
box-shadow:
0 0 0 3px rgba(34, 211, 238, 0.06),
0 0 18px rgba(34, 211, 238, 0.05);
}
This also complements the cyan and purple visual theme of the card.
Opening the Contact Form
JavaScript controls the component state by adding an is-open class to the panel.
const trigger = document.querySelector(
".cr-popup-contact__trigger"
);
const panel = document.querySelector(
".cr-popup-contact__panel"
);
trigger.addEventListener("click", () => {
trigger.style.display = "none";
trigger.setAttribute(
"aria-expanded",
"true"
);
panel.classList.add("is-open");
panel.setAttribute(
"aria-hidden",
"false"
);
});
Once the form opens, the original Contact Us button is hidden so the interface stays clean.
Closing the Form
The close button reverses the state and brings the Contact Us button back.
const closeButton = document.querySelector(
".cr-popup-contact__close"
);
closeButton.addEventListener("click", () => {
panel.classList.remove("is-open");
panel.setAttribute(
"aria-hidden",
"true"
);
trigger.setAttribute(
"aria-expanded",
"false"
);
trigger.style.display = "inline-flex";
});
Using the same state for the visual panel and ARIA attributes keeps the interaction more predictable.
Adding Keyboard Support
Users can also close the form using the Escape key.
document.addEventListener("keydown", (event) => {
if (
event.key === "Escape" &&
panel.classList.contains("is-open")
) {
panel.classList.remove("is-open");
panel.setAttribute(
"aria-hidden",
"true"
);
trigger.setAttribute(
"aria-expanded",
"false"
);
trigger.style.display = "inline-flex";
}
});
It's a small interaction, but it makes the component more comfortable for keyboard users.
Form Validation and Success State
For the demo, we can use the browser's native validation instead of building a custom validation library.
const form = document.querySelector(
".cr-popup-contact__form"
);
const success = document.querySelector(
".cr-popup-contact__success"
);
form.addEventListener("submit", (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
form.reset();
success.classList.add("is-visible");
setTimeout(() => {
success.classList.remove("is-visible");
}, 3000);
});
After a valid submission, the fields reset and a temporary success message appears.
The current component is a front-end demonstration. It does not actually send an email.
For a production project, the submit handler can be connected to your own API, WordPress handler, serverless function, or another backend service.
Responsive Design
The card uses a fluid width with a maximum size:
.cr-popup-contact__card {
width: min(100%, 460px);
}
We can then reduce padding and typography on smaller screens.
@media (max-width: 600px) {
.cr-popup-contact {
padding: 30px 12px;
}
.cr-popup-contact__panel.is-open
.cr-popup-contact__card {
padding: 26px 17px 18px;
}
.cr-popup-contact__content h2 {
font-size: 24px;
}
}
This keeps the component usable without creating horizontal overflow on mobile devices.
Reduced Motion Support
Animated interfaces should also respect the user's motion preferences.
@media (prefers-reduced-motion: reduce) {
.cr-popup-contact__trigger,
.cr-popup-contact__panel,
.cr-popup-contact__card,
.cr-popup-contact__close,
.cr-popup-contact__submit {
transition: none;
}
}
Users who have reduced motion enabled will get the same functionality without unnecessary transitions.
Final Result
The finished Glass Contact Form combines:
- Glass-style dark UI
- Cyan and purple glow effects
- Floating form labels
- Smooth open and close interactions
- Responsive layout
- Native form validation
- Success feedback
- Escape-key support
- Reduced-motion support
- Vanilla HTML, CSS, and JavaScript
The component can also be customized with different colors, fields, dimensions, animations, or a real form submission backend.
Try the Complete Glass Contact Form
The snippets above explain the main techniques behind the component.
For the live demo, complete HTML, CSS and JavaScript, and copy-ready source code, visit the full CodeRipple tutorial:
👉 https://getcoderipple.com/glass-contact-form/
You can also open the component in the CodeRipple Playground to test the live preview and customize the code for your own project.
Top comments (0)