Portfolio — Contact / Hire-me CTA + Form
A conversion-minded contact block for the foot of any portfolio. A bold Let’s work together headline sits beside a live availability badge, a copy-email button and a social row, paired with a compact hire-me form — name, email, project-type select and message — that validates inline on blur and submit. Vanilla JS shows friendly field errors, a character counter, a clipboard copy with toast, and an animated success state confirming a reply within 24h. Neutral Inter base, accessible, responsive.
MCP
Code
/* =================================================================
Portfolio — Contact / Hire-me CTA + Form
Neutral base palette, "Inter". Drop-in contact block.
================================================================= */
:root {
--ink: #16151a;
--ink-soft: #46434f;
--ink-mute: #76737f;
--line: #e7e5ea;
--line-strong: #d6d3dc;
--paper: #ffffff;
--paper-2: #faf9fb;
--paper-3: #f3f1f6;
--accent: #5b54ff;
--accent-ink: #4640e6;
--accent-soft: #eeedff;
--ok: #19a974;
--ok-ink: #0f7a55;
--ok-soft: #e7f8ef;
--danger: #d6334a;
--danger-soft: #fdecef;
--invert-bg: #16151a;
--invert-ink: #f6f5f9;
--invert-mute: #9c98a8;
--radius-sm: 10px;
--radius: 16px;
--radius-lg: 26px;
--shadow-sm: 0 1px 2px rgba(22, 21, 26, 0.06);
--shadow-md: 0 14px 40px -18px rgba(22, 21, 26, 0.28);
--shadow-lg: 0 36px 90px -40px rgba(22, 21, 26, 0.45);
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--maxw: 1120px;
--font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-display: "Inter Tight", "Inter", system-ui, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--font);
font-size: 16px;
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(120% 70% at 50% -10%, var(--paper-3), transparent 60%),
var(--paper-2);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
a {
color: inherit;
text-decoration: none;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
button {
font-family: inherit;
}
:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 3px;
border-radius: 6px;
}
.skip-link {
position: absolute;
left: 16px;
top: -64px;
z-index: 50;
padding: 10px 16px;
background: var(--ink);
color: #fff;
border-radius: var(--radius-sm);
font-weight: 600;
transition: top 0.18s var(--ease);
}
.skip-link:focus {
top: 16px;
}
/* ---------------- Page chrome ---------------- */
.page-head {
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(8px);
}
.page-head__inner {
max-width: var(--maxw);
margin: 0 auto;
padding: 22px 24px;
display: flex;
align-items: center;
gap: 16px;
}
.page-head__mark {
font-size: 1.6rem;
color: var(--accent);
line-height: 1;
}
.page-head__title {
margin: 0;
font-family: var(--font-display);
font-size: clamp(1.1rem, 2.6vw, 1.4rem);
font-weight: 800;
letter-spacing: -0.02em;
}
.page-head__sub {
margin: 2px 0 0;
color: var(--ink-mute);
font-size: 0.92rem;
}
.stack {
max-width: var(--maxw);
margin: 0 auto;
padding: 40px 24px 28px;
}
.page-foot {
max-width: var(--maxw);
margin: 0 auto;
padding: 8px 24px 56px;
color: var(--ink-mute);
font-size: 0.85rem;
}
/* ---------------- Layout shell ---------------- */
.contact {
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background: var(--paper);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.contact__grid {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
}
/* ---------------- Left rail (CTA) ---------------- */
.cta {
position: relative;
padding: clamp(32px, 4.5vw, 56px);
background:
radial-gradient(120% 90% at 0% 0%, var(--accent-soft), transparent 58%),
var(--paper);
border-right: 1px solid var(--line);
display: flex;
flex-direction: column;
}
.cta__eyebrow {
display: inline-flex;
align-items: center;
gap: 9px;
align-self: flex-start;
margin: 0 0 22px;
padding: 7px 14px;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
color: var(--accent-ink);
background: rgba(255, 255, 255, 0.7);
border: 1px solid var(--line-strong);
}
.cta__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(25, 169, 116, 0.5);
animation: cta-dot 2.4s ease-out infinite;
}
@keyframes cta-dot {
0% {
box-shadow: 0 0 0 0 rgba(25, 169, 116, 0.5);
}
70%,
100% {
box-shadow: 0 0 0 8px rgba(25, 169, 116, 0);
}
}
.cta__title {
margin: 0;
font-family: var(--font-display);
font-weight: 800;
letter-spacing: -0.04em;
line-height: 0.96;
font-size: clamp(2.6rem, 6.2vw, 4rem);
}
.cta__title-accent {
background: linear-gradient(95deg, var(--accent), #8a84ff);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}
.cta__lede {
margin: 20px 0 0;
max-width: 42ch;
color: var(--ink-soft);
font-size: 1.02rem;
}
.avail {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 16px;
margin: 26px 0 0;
}
.avail__badge {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 9px 16px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
color: var(--ok-ink);
background: var(--ok-soft);
border: 1px solid rgba(25, 169, 116, 0.3);
}
.avail__pulse {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--ok);
position: relative;
}
.avail__pulse::after {
content: "";
position: absolute;
inset: -4px;
border-radius: 50%;
border: 1.5px solid var(--ok);
opacity: 0;
animation: avail-ring 2.4s ease-out infinite;
}
@keyframes avail-ring {
0% {
transform: scale(0.5);
opacity: 0.7;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
.avail__note {
font-size: 0.86rem;
color: var(--ink-mute);
font-weight: 500;
}
.copy-email {
display: inline-flex;
align-items: center;
gap: 10px;
align-self: flex-start;
margin: 26px 0 0;
padding: 12px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--line-strong);
background: var(--paper-2);
color: var(--ink);
font-size: 0.95rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
cursor: pointer;
transition:
border-color 0.18s var(--ease),
background 0.18s var(--ease),
color 0.18s var(--ease),
transform 0.18s var(--ease);
}
.copy-email:hover {
border-color: var(--accent);
color: var(--accent-ink);
background: var(--accent-soft);
}
.copy-email:active {
transform: translateY(1px);
}
.copy-email__icon {
font-size: 1.05rem;
}
.copy-email__hint {
margin-left: 4px;
padding: 2px 9px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--ink-mute);
background: var(--paper-3);
border: 1px solid var(--line);
}
.copy-email.is-copied {
border-color: var(--ok);
color: var(--ok-ink);
background: var(--ok-soft);
}
.copy-email.is-copied .copy-email__hint {
color: var(--ok-ink);
background: rgba(25, 169, 116, 0.14);
border-color: rgba(25, 169, 116, 0.3);
}
.social {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 22px 0 0;
}
.social__link {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 13px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
color: var(--ink-soft);
background: var(--paper-2);
border: 1px solid var(--line);
transition:
border-color 0.18s var(--ease),
color 0.18s var(--ease),
transform 0.18s var(--ease);
}
.social__link span {
font-size: 0.78rem;
font-weight: 800;
color: var(--accent-ink);
}
.social__link:hover {
border-color: var(--accent);
color: var(--ink);
transform: translateY(-2px);
}
.cta__stats {
display: flex;
gap: 36px;
margin: auto 0 0;
padding-top: 28px;
}
.cta__stats dt {
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--ink-mute);
}
.cta__stats dd {
margin: 3px 0 0;
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 800;
letter-spacing: -0.02em;
}
/* ---------------- Right panel (form) ---------------- */
.panel {
position: relative;
padding: clamp(32px, 4.5vw, 56px);
background: var(--paper);
}
.form__title {
margin: 0;
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.form__sub {
margin: 4px 0 24px;
color: var(--ink-mute);
font-size: 0.95rem;
}
.field {
margin: 0 0 18px;
}
.field__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.field__label {
display: block;
margin: 0 0 7px;
font-size: 0.86rem;
font-weight: 600;
color: var(--ink-soft);
}
.field__count {
font-size: 0.78rem;
color: var(--ink-mute);
font-variant-numeric: tabular-nums;
}
.field__input {
width: 100%;
padding: 12px 14px;
border: 1px solid var(--line-strong);
border-radius: var(--radius-sm);
background: var(--paper-2);
color: var(--ink);
font-family: inherit;
font-size: 0.97rem;
line-height: 1.4;
transition:
border-color 0.16s var(--ease),
box-shadow 0.16s var(--ease),
background 0.16s var(--ease);
}
.field__input::placeholder {
color: #aaa7b2;
}
.field__input:hover {
border-color: var(--ink-mute);
}
.field__input:focus {
outline: none;
border-color: var(--accent);
background: var(--paper);
box-shadow: 0 0 0 4px var(--accent-soft);
}
.field__area {
resize: vertical;
min-height: 96px;
}
.field__select-wrap {
position: relative;
}
.field__select {
appearance: none;
-webkit-appearance: none;
cursor: pointer;
padding-right: 40px;
}
.field__select:invalid {
color: var(--ink-mute);
}
.field__chevron {
position: absolute;
top: 50%;
right: 14px;
transform: translateY(-55%);
font-size: 1.15rem;
color: var(--ink-mute);
pointer-events: none;
}
/* Error state */
.field.is-invalid .field__input {
border-color: var(--danger);
background: var(--danger-soft);
}
.field.is-invalid .field__input:focus {
box-shadow: 0 0 0 4px rgba(214, 51, 74, 0.14);
}
.field__error {
display: flex;
align-items: center;
gap: 6px;
margin: 7px 0 0;
color: var(--danger);
font-size: 0.82rem;
font-weight: 600;
}
.field__error::before {
content: "⚠";
font-size: 0.85rem;
}
/* Valid hint */
.field.is-valid .field__input {
border-color: rgba(25, 169, 116, 0.55);
}
.form__submit {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
margin: 6px 0 0;
padding: 14px 20px;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: var(--ink);
color: #fff;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition:
background 0.18s var(--ease),
transform 0.18s var(--ease),
box-shadow 0.18s var(--ease);
}
.form__submit:hover {
background: var(--accent);
box-shadow: 0 14px 30px -14px var(--accent);
}
.form__submit:active {
transform: translateY(1px) scale(0.995);
}
.form__submit.is-loading {
pointer-events: none;
opacity: 0.85;
}
.form__submit.is-loading .form__submit-label::after {
content: "…";
}
.form__submit-icon {
transition: transform 0.18s var(--ease);
}
.form__submit:hover .form__submit-icon {
transform: translateX(4px);
}
.form__legal {
margin: 14px 0 0;
color: var(--ink-mute);
font-size: 0.8rem;
text-align: center;
}
/* ---------------- Success state ---------------- */
.success {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: clamp(32px, 5vw, 56px);
background:
radial-gradient(100% 80% at 50% 0%, var(--ok-soft), var(--paper) 70%);
animation: success-in 0.4s var(--ease) both;
}
@keyframes success-in {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
.success__mark {
display: grid;
place-items: center;
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--paper);
box-shadow: var(--shadow-md);
border: 1px solid rgba(25, 169, 116, 0.25);
}
.success__circle {
fill: none;
stroke: var(--ok);
stroke-width: 3;
opacity: 0.25;
}
.success__check {
fill: none;
stroke: var(--ok);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: success-check 0.5s var(--ease) 0.15s forwards;
}
@keyframes success-check {
to {
stroke-dashoffset: 0;
}
}
.success__title {
margin: 20px 0 0;
font-family: var(--font-display);
font-size: 1.45rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.success__text {
margin: 8px 0 0;
max-width: 34ch;
color: var(--ink-soft);
}
.success__again {
margin: 24px 0 0;
padding: 11px 20px;
border-radius: 999px;
border: 1px solid var(--line-strong);
background: var(--paper);
color: var(--ink);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition:
border-color 0.18s var(--ease),
color 0.18s var(--ease);
}
.success__again:hover {
border-color: var(--accent);
color: var(--accent-ink);
}
/* ---------------- Toast ---------------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
z-index: 60;
max-width: min(90vw, 360px);
padding: 13px 20px;
border-radius: 999px;
background: var(--ink);
color: #fff;
font-size: 0.9rem;
font-weight: 600;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition:
opacity 0.25s var(--ease),
transform 0.25s var(--ease);
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------------- Responsive ---------------- */
@media (max-width: 860px) {
.contact__grid {
grid-template-columns: 1fr;
}
.cta {
border-right: none;
border-bottom: 1px solid var(--line);
}
.cta__stats {
margin-top: 28px;
}
}
@media (max-width: 520px) {
.stack {
padding: 24px 14px 16px;
}
.page-head__inner {
padding: 18px 16px;
}
.cta,
.panel {
padding: 26px 20px;
}
.cta__title {
font-size: clamp(2.3rem, 11vw, 3rem);
}
.cta__stats {
gap: 28px;
}
.copy-email {
width: 100%;
justify-content: flex-start;
}
.copy-email__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-email__hint {
margin-left: auto;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.success__check {
stroke-dashoffset: 0;
}
}/* =================================================================
Portfolio — Contact / Hire-me CTA + Form
Vanilla JS: inline validation, copy-email, success state, toast.
================================================================= */
(function () {
"use strict";
/* ---------------- Toast helper ---------------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 2600);
}
/* ---------------- Copy email ---------------- */
var copyBtn = document.getElementById("copyEmail");
if (copyBtn) {
var hint = copyBtn.querySelector(".copy-email__hint");
var defaultHint = hint ? hint.textContent : "Copy";
copyBtn.addEventListener("click", function () {
var email = copyBtn.getAttribute("data-email") || "";
function done() {
copyBtn.classList.add("is-copied");
if (hint) hint.textContent = "Copied";
toast("Email copied — " + email);
window.setTimeout(function () {
copyBtn.classList.remove("is-copied");
if (hint) hint.textContent = defaultHint;
}, 2000);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(email).then(done, fallbackCopy);
} else {
fallbackCopy();
}
function fallbackCopy() {
try {
var ta = document.createElement("textarea");
ta.value = email;
ta.setAttribute("readonly", "");
ta.style.position = "absolute";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
done();
} catch (err) {
toast("Couldn’t copy — " + email);
}
}
});
}
/* ---------------- Form validation ---------------- */
var form = document.getElementById("contactForm");
if (!form) return;
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
var validators = {
name: function (v) {
if (!v.trim()) return "Please add your name.";
if (v.trim().length < 2) return "That name looks a little short.";
return "";
},
email: function (v) {
if (!v.trim()) return "An email so I can reply.";
if (!EMAIL_RE.test(v.trim())) return "That doesn’t look like an email.";
return "";
},
ptype: function (v) {
if (!v) return "Pick the kind of project.";
return "";
},
message: function (v) {
if (!v.trim()) return "Tell me a little about it.";
if (v.trim().length < 12) return "A few more words would help.";
return "";
}
};
var fields = ["name", "email", "ptype", "message"].map(function (id) {
var input = document.getElementById(id);
return {
id: id,
input: input,
wrap: input ? input.closest(".field") : null,
error: document.getElementById(id + "-err")
};
});
function setState(field, message) {
if (!field.wrap) return Boolean(message);
var invalid = Boolean(message);
field.wrap.classList.toggle("is-invalid", invalid);
field.wrap.classList.toggle("is-valid", !invalid && field.input.value !== "");
if (field.error) {
field.error.textContent = message;
field.error.hidden = !invalid;
}
if (field.input) {
field.input.setAttribute("aria-invalid", invalid ? "true" : "false");
}
return invalid;
}
function validateField(field) {
var fn = validators[field.id];
var msg = fn ? fn(field.input.value) : "";
return setState(field, msg);
}
// Validate on blur; clear errors live once a field becomes valid again.
fields.forEach(function (field) {
if (!field.input) return;
field.input.addEventListener("blur", function () {
validateField(field);
});
field.input.addEventListener("input", function () {
if (field.wrap && field.wrap.classList.contains("is-invalid")) {
validateField(field);
}
});
if (field.id === "ptype") {
field.input.addEventListener("change", function () {
validateField(field);
});
}
});
/* ---------------- Character counter ---------------- */
var message = document.getElementById("message");
var count = document.getElementById("count");
if (message && count) {
var max = message.getAttribute("maxlength") || 600;
var update = function () {
count.textContent = message.value.length + " / " + max;
};
message.addEventListener("input", update);
update();
}
/* ---------------- Submit ---------------- */
var submitBtn = document.getElementById("submitBtn");
var success = document.getElementById("success");
var successText = document.getElementById("successText");
var resetBtn = document.getElementById("resetBtn");
form.addEventListener("submit", function (e) {
e.preventDefault();
var firstInvalid = null;
fields.forEach(function (field) {
var invalid = validateField(field);
if (invalid && !firstInvalid) firstInvalid = field;
});
if (firstInvalid) {
if (firstInvalid.input) firstInvalid.input.focus();
toast("Please fix the highlighted fields.");
return;
}
// Simulate a send.
if (submitBtn) {
submitBtn.classList.add("is-loading");
var label = submitBtn.querySelector(".form__submit-label");
if (label) label.textContent = "Sending";
}
window.setTimeout(function () {
var name = (document.getElementById("name").value || "").trim();
var first = name.split(/\s+/)[0] || "there";
if (successText) {
successText.textContent =
"Got it, " + first + " — your message is on its way. I’ll reply within 24 hours.";
}
if (success) success.hidden = false;
form.style.visibility = "hidden";
toast("Message sent — thanks!");
if (success) {
var title = success.querySelector(".success__title");
if (title) title.setAttribute("tabindex", "-1");
if (title) title.focus();
}
}, 700);
});
/* ---------------- Reset / send another ---------------- */
if (resetBtn) {
resetBtn.addEventListener("click", function () {
form.reset();
fields.forEach(function (field) {
if (field.wrap) field.wrap.classList.remove("is-invalid", "is-valid");
if (field.error) field.error.hidden = true;
if (field.input) field.input.setAttribute("aria-invalid", "false");
});
if (success) success.hidden = true;
form.style.visibility = "";
if (submitBtn) {
submitBtn.classList.remove("is-loading");
var label = submitBtn.querySelector(".form__submit-label");
if (label) label.textContent = "Send message";
}
if (count && message) {
count.textContent = "0 / " + (message.getAttribute("maxlength") || 600);
}
var nameInput = document.getElementById("name");
if (nameInput) nameInput.focus();
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Portfolio — Contact / Hire-me CTA + Form</title>
<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=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@600;700;800;900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#contact">Skip to contact form</a>
<header class="page-head">
<div class="page-head__inner">
<span class="page-head__mark" aria-hidden="true">◖</span>
<div>
<h1 class="page-head__title">Contact / Hire-me CTA</h1>
<p class="page-head__sub">
A conversion-minded contact block — paste it at the foot of any portfolio.
</p>
</div>
</div>
</header>
<main class="stack">
<section id="contact" class="contact" aria-labelledby="contact-title">
<div class="contact__grid">
<!-- ============================================ -->
<!-- Left rail — CTA, availability, copy-email -->
<!-- ============================================ -->
<div class="cta">
<p class="cta__eyebrow">
<span class="cta__dot" aria-hidden="true"></span>
Open to freelance & full-time
</p>
<h2 id="contact-title" class="cta__title">
Let’s work<br />
<span class="cta__title-accent">together.</span>
</h2>
<p class="cta__lede">
I’m Maya Okafor, a product designer building calm, useful
interfaces. Tell me about your project — I read every message and
reply within a day.
</p>
<div class="avail" role="group" aria-label="Availability">
<span class="avail__badge">
<span class="avail__pulse" aria-hidden="true"></span>
Available from August
</span>
<span class="avail__note">2 project slots left this quarter</span>
</div>
<button
type="button"
class="copy-email"
id="copyEmail"
data-email="hey@mayaokafor.studio"
>
<span class="copy-email__icon" aria-hidden="true">✉</span>
<span class="copy-email__label">hey@mayaokafor.studio</span>
<span class="copy-email__hint" aria-hidden="true">Copy</span>
</button>
<ul class="social" aria-label="Find me elsewhere">
<li>
<a class="social__link" href="#" aria-label="Maya on Dribbble">
<span aria-hidden="true">◐</span> Dribbble
</a>
</li>
<li>
<a class="social__link" href="#" aria-label="Maya on LinkedIn">
<span aria-hidden="true">in</span> LinkedIn
</a>
</li>
<li>
<a class="social__link" href="#" aria-label="Maya on Read.cv">
<span aria-hidden="true">▤</span> Read.cv
</a>
</li>
</ul>
<dl class="cta__stats">
<div>
<dt>Reply time</dt>
<dd>< 24h</dd>
</div>
<div>
<dt>Based in</dt>
<dd>Lisbon · GMT+1</dd>
</div>
</dl>
</div>
<!-- ============================================ -->
<!-- Right — the form / success state -->
<!-- ============================================ -->
<div class="panel">
<form class="form" id="contactForm" novalidate>
<h3 class="form__title">Start a conversation</h3>
<p class="form__sub">No pitch decks required — a sentence works.</p>
<div class="field">
<label class="field__label" for="name">Your name</label>
<input
class="field__input"
id="name"
name="name"
type="text"
autocomplete="name"
placeholder="Jordan Rivera"
required
aria-describedby="name-err"
/>
<p class="field__error" id="name-err" hidden></p>
</div>
<div class="field">
<label class="field__label" for="email">Email</label>
<input
class="field__input"
id="email"
name="email"
type="email"
autocomplete="email"
placeholder="you@company.com"
required
aria-describedby="email-err"
/>
<p class="field__error" id="email-err" hidden></p>
</div>
<div class="field">
<label class="field__label" for="ptype">Project type</label>
<div class="field__select-wrap">
<select
class="field__input field__select"
id="ptype"
name="ptype"
required
aria-describedby="ptype-err"
>
<option value="" disabled selected>Pick one…</option>
<option value="product">Product / app design</option>
<option value="website">Marketing website</option>
<option value="designsystem">Design system</option>
<option value="brand">Brand & identity</option>
<option value="advisory">Advisory / audit</option>
<option value="other">Something else</option>
</select>
<span class="field__chevron" aria-hidden="true">⌄</span>
</div>
<p class="field__error" id="ptype-err" hidden></p>
</div>
<div class="field">
<div class="field__row">
<label class="field__label" for="message">Message</label>
<span class="field__count" id="count" aria-hidden="true">0 / 600</span>
</div>
<textarea
class="field__input field__area"
id="message"
name="message"
rows="4"
maxlength="600"
placeholder="What are you building, and what does success look like?"
required
aria-describedby="message-err"
></textarea>
<p class="field__error" id="message-err" hidden></p>
</div>
<button type="submit" class="form__submit" id="submitBtn">
<span class="form__submit-label">Send message</span>
<span class="form__submit-icon" aria-hidden="true">→</span>
</button>
<p class="form__legal">
By sending, you agree to a friendly reply. No newsletters, ever.
</p>
</form>
<!-- Success state, revealed on submit -->
<div class="success" id="success" hidden role="status" aria-live="polite">
<span class="success__mark" aria-hidden="true">
<svg viewBox="0 0 52 52" width="44" height="44" focusable="false">
<circle class="success__circle" cx="26" cy="26" r="24" />
<path class="success__check" d="M14 27 l8 8 l16 -18" />
</svg>
</span>
<h3 class="success__title">Thanks — I’ll reply within 24h</h3>
<p class="success__text" id="successText">
Your message is on its way. Keep an eye on your inbox.
</p>
<button type="button" class="success__again" id="resetBtn">
Send another message
</button>
</div>
</div>
</div>
</section>
</main>
<footer class="page-foot">
Illustrative portfolio — fictional person and projects.
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Contact / Hire-me CTA + Form
A drop-in contact section built to convert. On the left, a bold Let’s work together headline anchors a friendly pitch, a pulsing availability badge (“Available from August · 2 slots left”), a one-click copy-email button, and a row of social links. On the right, a compact hire-me form collects a name, email, project-type select and message — everything you need to start a conversation, nothing you don’t.
Every interaction works. Fields validate on blur and on submit with plain-language errors (“That doesn’t look like an email.”), clear once corrected, and the message box carries a live character counter. Submitting flips the panel to an animated success state — a drawn checkmark and a personalised “Thanks — I’ll reply within 24h”, with a Send another message reset. The email button copies to the clipboard and confirms with a toast, falling back gracefully where the Clipboard API is unavailable.
It ships on the neutral Inter portfolio base, collapses from two columns to one on tablets, keeps WCAG AA contrast with visible focus rings and a skip link, moves focus to the success heading for screen readers, and respects prefers-reduced-motion. No images or external assets beyond the Google Fonts link.
Illustrative portfolio — fictional person and projects.