
এই লেখার কোড একটি শিক্ষামূলক, front-end-only ডেমো। এটি কোনো বার্তা পাঠায় না, কোনো API-তে request করে না এবং কোনো backend submit নেই। Submit চাপলে সফল দেখালেও স্পষ্টভাবে “NOT sent” লেখা হয়—তাই এটিকে production contact system মনে করবেন না।
কেন contact form-এ শুধু সুন্দর input যথেষ্ট নয়
Contact form দেখতে ছোট হলেও এটি ব্যবহারকারীর ভাষা, browser-এর native validation, keyboard navigation, assistive technology এবং ভবিষ্যৎ server নিরাপত্তার মাঝখানে একটি চুক্তি তৈরি করে। একটি input-এর পাশে placeholder বসালেই সেই চুক্তি পূর্ণ হয় না। Placeholder ফাঁকা হলে label হারিয়ে যায়; লেবেল না থাকলে screen reader ব্যবহারকারী কোন তথ্য চাইছে বুঝতে পারেন না; error কেবল লাল border হলে রঙ না দেখা বা কম contrast-এর ব্যবহারকারী কারণটি জানতে পারেন না। তাই এখানে লক্ষ্য হল—একই markup থেকে আগে HTML-এর স্বাভাবিক সুবিধা পাওয়া, পরে অল্প JavaScript দিয়ে বার্তা ও interaction উন্নত করা।
এই article-এ আমরা নাম, email, বিষয় এবং বার্তার চারটি field দিয়ে একটি ছোট contact form বানাব। প্রতিটি field-এর দৃশ্যমান label থাকবে, required এবং type="email" browser-কে প্রাথমিক constraint দেবে, CSS :focus-visible keyboard focus আলাদা করে দেখাবে, আর aria-live status ফলাফল জানাবে। JavaScript প্রথমে native validity-কে সম্মান করবে; invalid হলে browser-এর reportValidity() চালিয়ে প্রথম ভুল field-এ মনোযোগ দেবে। সব ঠিক থাকলে কোনো network call নয়, কেবল demonstration message দেখাবে। এই progressive নকশায় HTML constraints (required, email ও minlength) native validation-এর নিয়ম হিসেবে থাকে। তবে এই local-only demo-তে submit button JavaScript চালু না হওয়া পর্যন্ত disabled থাকে; তাই JavaScript বন্ধ থাকলে কোনো submission বা success message হয় না, এবং কোনো বার্তাও পাঠানো হয় না।
প্রথম সিদ্ধান্ত: form-এর semantic চুক্তি
<form> element ব্যবহারকারীর interactive controls-এর একটি document section। বাস্তব form-এ action ও method দিয়ে endpoint ও HTTP আচরণ নির্ধারিত হয়। এই শিক্ষামূলক demo-তে method="dialog" রাখা হয়েছে, কিন্তু form-টির কোনো <dialog> ancestor নেই; তাই এটি dialog close করার form নয় এবং browser-এর কোনো form network submissionও হবে না। JavaScript listener validation ও demo status চালায়, কিন্তু no-network নিশ্চয়তার একমাত্র ভিত্তি preventDefault() নয়। ভবিষ্যতে backend যোগ করার সময় এই demo-র method, message ও privacy notice নতুন করে লিখতে হবে।
প্রতিটি label-এর for value তার input-এর id-এর সঙ্গে মেলে। এতে label-এ click করলে field focus হয় এবং screen reader relationship বুঝতে পারে। name attribute বাদ দিইনি: আজ request না গেলেও বাস্তব backend-এ কোন key পাঠানো হবে সেটি name নির্ধারণ করে। Name, email ও message-এ autocomplete-এর অর্থপূর্ণ মান দিয়েছি; subject-এর জন্য off বা custom value ব্যবহারের বদলে browser-কে অপ্রয়োজনীয় autofill না করতে বলেছি। aria-describedby field-কে ছোট hint এবং error-এর সঙ্গে যুক্ত করবে, যদিও error element শুরুতে hidden।
দেখুন MDN-এর form element reference। সেখানে action, method, enctype, novalidate এবং form-এর native আচরণ সম্পর্কে browser-সম্মত ব্যাখ্যা আছে। এই article-এ novalidate ব্যবহার করা হয়নি, কারণ native constraint validation-ই আমাদের প্রথম প্রতিরক্ষা।
Field বাছাই এবং constraint-এর যুক্তি
নামের ক্ষেত্রে minlength="2" দিয়েছি—এক অক্ষরের নাম সবসময় ভুল নয়, কিন্তু এই sample-এর product rule হিসেবে দুই অক্ষর চেয়েছি। Email input-এ type="email" browser-কে email syntax যাচাই করতে সাহায্য করে, আর maxlength অস্বাভাবিক দীর্ঘ input কমায়। Subject-এ required এবং সীমিত maxlength; বার্তার textarea-তে minlength="20" রেখেছি যাতে “হাই” ধরনের অস্পষ্ট request না যায়। এটি কোনো universal truth নয়—বাস্তব product-এর ভাষা, ব্যবহারকারী এবং business rule অনুযায়ী সীমা বদলাবেন।
required শুধু UI সাজানোর চিহ্ন নয়; browser validity state-এ valueMissing সেট করে। type=email ভুল syntax হলে typeMismatch তৈরি করে। minlength ও maxlength client-side feedback উন্নত করে, কিন্তু malicious বা hand-crafted HTTP request আটকায় না। একজন ব্যবহারকারী DevTools দিয়ে HTML বদলাতে পারেন, কিংবা form বাদ দিয়ে সরাসরি request পাঠাতে পারেন। সেই কারণে client constraint-কে সুবিধা হিসেবে দেখুন, security boundary হিসেবে নয়।
Error-এর জন্য আমি প্রতিটি field-এর নিচে একটি <p> রেখেছি। JavaScript error string লিখবে এবং hidden খুলবে; aria-describedby আগে থেকেই সেই node-কে সম্পর্কিত করে। role="alert" সব error-এ দিলে typing-এর সময় অতিরিক্ত interruption হতে পারে, তাই field error-কে polite live region এবং আলাদা global status-কে role="status" রাখা হয়েছে। Submit ব্যর্থ হলে browser-এর native popup এবং দৃশ্যমান error—দুটির মধ্যে বিরোধ না তৈরি করতে প্রথমে reportValidity() ব্যবহার করেছি, তারপর custom message কেবল প্রয়োজনীয় ক্ষেত্রে আপডেট করেছি।
CSS: focus এবং error-কে শুধু রঙে আটকে না রাখা
ফর্মের visual hierarchy ইচ্ছাকৃতভাবে ছোট: card, label, input, hint, error, action। এখানে কোনো generic responsive design tutorial নেই; CSS-এর কেন্দ্র field state। :focus-visible কেবল keyboard বা modality-তে দরকার হলে outline দেখায়, তাই mouse user-এর অপ্রত্যাশিত ring-এর বদলে keyboard user স্পষ্ট indication পান। Outline সরিয়ে ফেললে তা অবশ্যই নিজস্ব, দৃশ্যমান alternative দিয়ে পূরণ করতে হবে—এখানে outline বজায় রেখেছি।
:user-invalid সব browser-এ একইভাবে পুরোনো নয়, তাই demo-তে JavaScript-এর .has-error class-এর সঙ্গে :focus এবং :required ব্যবহার করেছি। Error field-এ border color বদলেছে, কিন্তু পাশে text message এবং icon-এর বদলে “সমস্যা” শব্দও আছে—এতে color-only dependency কমে। High-contrast mode বা forced-colors-এ user-agent-এর system color সম্মান করতে @media (forced-colors: active)-এ CanvasText ও Highlight ব্যবহার করেছি। Reduced-motion preference-এ animation নেই; কারণ validation message-এর জন্য animation কোনো দরকারি তথ্য যোগ করে না।
Progressive JavaScript flow
Script-এর flow চার ধাপে পড়ুন। প্রথমে form ও field references নেওয়া হয়। দ্বিতীয়ত, showFieldError() value, validity এবং custom message দেখে text বসায়। তৃতীয়ত, input event-এ user আবার লেখা শুরু করলে stale error সরানো হয়; এতে আগের ভুল বার্তা আটকে থাকে না। চতুর্থত, submit handler form.checkValidity() দিয়ে সামগ্রিক ফল পায়। false হলে form.reportValidity() browser-কে native message দেখাতে দেয়, তারপর প্রথম invalid element-এ focus() করা হয়। এই call-গুলো Constraint Validation API-এর অংশ; বিস্তারিত MDN Constraint validation guide পড়ুন।
সব field valid হলে handler event.preventDefault()-এর পরও কোনো fetch, XMLHttpRequest, form submit বা storage API কল করে না। status.textContent-এ লেখা হয়: “Demo success: validation passed, but message NOT sent.” এই বাক্যটি ইচ্ছাকৃতভাবে success শব্দ এবং non-delivery দুটোই বলে। Form reset না করায় learner চাইলে input দেখে DOM inspect করতে পারেন; reset করলে value হারানোর ঝুঁকি থাকে। বাস্তব app-এ server response আসা পর্যন্ত button disabled, idempotency এবং retry policy লাগতে পারে—এই demo সেগুলোর ভান করে না।
Custom validity কখন ব্যবহার করবেন? দুই field-এর সম্পর্ক যাচাই করতে setCustomValidity() ভালো উপায়, যেমন confirm email বা date range। কিন্তু এই sample-এ প্রতিটি field-এর rule native attribute-এই যথেষ্ট, তাই কৃত্রিম custom validity বসাইনি। এতে একটি গুরুত্বপূর্ণ শিক্ষা থাকে: JavaScript দিয়ে browser-এর built-in constraint আবার কপি না করে, native rule-কে feedback layer দিয়ে সমর্থন করুন। একই সঙ্গে validation message localization, browser-specific wording এবং accessible explanation-এর ভারসাম্য রাখুন।
সম্পূর্ণ standalone index.html
নিচের একটিমাত্র file save করে সরাসরি browser-এ খুলুন। কোনো package, build step বা external JavaScript দরকার নেই। Internet ছাড়াও demo চলবে, কারণ CSS ও script inline। method="dialog" এখানে কেবল demo-র জন্য; কোনো <dialog> ancestor না থাকায় form কোনো network submission করবে না। JavaScript চালু হলে তবেই demo button সক্রিয় হয়।
<!doctype html>
<html lang="bn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accessible Contact Form Demo</title>
<style>
:root { color-scheme: light; font-family: system-ui, sans-serif; line-height: 1.5; }
* { box-sizing: border-box; }
body { margin: 0; background: #eef2ff; color: #172033; }
main { width: min(100% - 2rem, 42rem); margin: 4rem auto; }
.card { background: #fff; padding: clamp(1.25rem, 4vw, 2.5rem); border: 1px solid #cbd5e1; border-radius: 1rem; box-shadow: 0 1rem 2rem #17203318; }
h1 { margin-top: 0; }
.notice { padding: .8rem 1rem; border-left: .3rem solid #7c3aed; background: #f5f3ff; }
.field { margin-block: 1.25rem; }
label { display: block; font-weight: 700; margin-bottom: .35rem; }
input, textarea { width: 100%; border: 2px solid #94a3b8; border-radius: .5rem; padding: .7rem .8rem; font: inherit; color: inherit; background: #fff; }
textarea { min-height: 9rem; resize: vertical; }
input:focus-visible, textarea:focus-visible, button:focus-visible { outline: 3px solid #f59e0b; outline-offset: 3px; }
input:invalid:not(:placeholder-shown), textarea:invalid:not(:placeholder-shown), .has-error { border-color: #b42318; }
.hint, .error { font-size: .9rem; margin: .35rem 0 0; }
.hint { color: #475569; }
.error { color: #b42318; font-weight: 650; }
button { border: 0; border-radius: .5rem; padding: .75rem 1.1rem; font: inherit; font-weight: 700; cursor: pointer; background: #3730a3; color: #fff; }
button:hover { background: #312e81; }
#status { min-height: 1.6rem; margin-top: 1rem; font-weight: 700; }
.success { color: #166534; }
@media (forced-colors: active) { input, textarea { border: 2px solid CanvasText; } input:focus-visible, textarea:focus-visible, button:focus-visible { outline-color: Highlight; } }
</style>
</head>
<body>
<main>
<section class="card" aria-labelledby="form-title">
<h1 id="form-title">যোগাযোগের ডেমো</h1>
<p class="notice">এটি শেখার জন্য front-end demo। Submit করলে কোনো বার্তা পাঠানো হবে না।</p>
<form id="contact-form" method="dialog">
<div class="field">
<label for="name">নাম <span aria-hidden="true">*</span></label>
<input id="name" name="name" type="text" autocomplete="name" minlength="2" maxlength="80" required aria-describedby="name-hint name-error">
<p id="name-hint" class="hint">কমপক্ষে ২ অক্ষর লিখুন।</p>
<p id="name-error" class="error" hidden></p>
</div>
<div class="field">
<label for="email">ইমেইল <span aria-hidden="true">*</span></label>
<input id="email" name="email" type="email" autocomplete="email" maxlength="254" required aria-describedby="email-hint email-error">
<p id="email-hint" class="hint">উত্তরের জন্য একটি বৈধ email দিন।</p>
<p id="email-error" class="error" hidden></p>
</div>
<div class="field">
<label for="subject">বিষয় <span aria-hidden="true">*</span></label>
<input id="subject" name="subject" type="text" maxlength="120" required aria-describedby="subject-hint subject-error">
<p id="subject-hint" class="hint">আপনার প্রশ্নের সংক্ষিপ্ত শিরোনাম।</p>
<p id="subject-error" class="error" hidden></p>
</div>
<div class="field">
<label for="message">বার্তা <span aria-hidden="true">*</span></label>
<textarea id="message" name="message" minlength="20" maxlength="2000" required aria-describedby="message-hint message-error"></textarea>
<p id="message-hint" class="hint">কমপক্ষে ২০ অক্ষরে সমস্যাটি লিখুন।</p>
<p id="message-error" class="error" hidden></p>
</div>
<button id="submit-button" type="submit" disabled>ডেমো যাচাই করুন</button>
<p id="status" role="status" aria-live="polite"></p>
</form>
</section>
</main>
<script>
const form = document.querySelector('#contact-form');
const fields = [...form.querySelectorAll('input, textarea')];
const status = document.querySelector('#status');
const submitButton = document.querySelector('#submit-button');
function messageFor(field) {
if (field.validity.valueMissing) return 'এই ঘরটি পূরণ করুন।';
if (field.validity.typeMismatch) return 'একটি বৈধ email format দিন।';
if (field.validity.tooShort) return `কমপক্ষে ${field.minLength} অক্ষর লিখুন।`;
if (field.validity.tooLong) return `সর্বোচ্চ ${field.maxLength} অক্ষর রাখা যাবে।`;
return '';
}
function showFieldError(field) {
const error = document.querySelector(`#${field.id}-error`);
const text = messageFor(field);
error.textContent = text;
error.hidden = !text;
field.classList.toggle('has-error', Boolean(text));
field.setAttribute('aria-invalid', text ? 'true' : 'false');
return !text;
}
fields.forEach((field) => {
field.addEventListener('input', () => {
showFieldError(field);
if (fields.every((item) => item.validity.valid)) status.textContent = '';
});
});
form.addEventListener('submit', (event) => {
event.preventDefault(); // Demo: deliberately prevents any network submission.
status.className = '';
const valid = form.checkValidity();
fields.forEach(showFieldError);
if (!valid) {
form.reportValidity();
const firstInvalid = fields.find((field) => !field.validity.valid);
firstInvalid?.focus();
status.textContent = 'কিছু ঘর ঠিক করুন, তারপর আবার চেষ্টা করুন।';
return;
}
status.className = 'success';
status.textContent = 'Demo success: validation passed, but message NOT sent.';
});
// JavaScript is required for this local-only demonstration button.
submitButton.disabled = false;
</script>
</body>
</html>
কোড পড়ার সময় যে ছোট সিদ্ধান্তগুলো খেয়াল করবেন
aria-invalid="false" শুরুতেই লেখা নেই; user প্রথম submit বা input করার পর script সেট করে। এতে untouched field-এ শুরুতেই error state ঘোষণা হয় না। Error paragraph-এর hidden attribute screen reader এবং visual user—দুজনের জন্যই শুরুতে content সরিয়ে রাখে। Text বদলানোর পর aria-describedby-এর সম্পর্ক একই থাকে, তাই error প্রকাশিত হলে assistive technology সেটি পড়তে পারে। role="status" polite হওয়ায় typing-এর মাঝখানে জরুরি alert-এর মতো interrupt করবে না। Submit-এর পরে browser native bubble দেখালেও আমাদের status-এ সংক্ষিপ্ত নির্দেশ থাকে, ফলে keyboard user বুঝতে পারেন যে কী করবেন।
input listener-এ আমরা custom regular expression দিয়ে email বানাইনি। Email format-এর জন্য browser-এর type=email যথেষ্ট প্রাথমিক rule; messageFor()-এর typeMismatch message কেবল বাংলা feedback। Browser-এর validity property-কে source of truth রাখায় rule এবং message আলাদা হয়ে অসঙ্গত হওয়ার সম্ভাবনা কম। maxlength user input-এ browser সাধারণত সীমা মানে, কিন্তু server-এ একই সীমা আবার enforce করতে হবে। কোনো field-এ whitespace-only policy দরকার হলে backend-এ trim ও business validation করুন; এই demo সেই policy গোপনে ধরে নেয় না।
Validation test cases: ইচ্ছাকৃতভাবে ভাঙুন
প্রথম test-এ সব ঘর খালি রেখে button চাপুন। Browser প্রথম required field দেখাবে, script সব error paragraph প্রকাশ করবে এবং status বলবে কিছু ঘর ঠিক করতে। এরপর name-এ এক অক্ষর দিন; tooShort message দেখা উচিত। দুই বা তার বেশি অক্ষর দিলে সেই error অদৃশ্য হবে। এই sequence-টি নিশ্চিত করে যে invalid state আটকে নেই এবং user input-এ feedback আপডেট হচ্ছে।
দ্বিতীয় test-এ email-এ hello লিখুন। Browser typeMismatch জানাবে এবং বাংলা message “বৈধ email format” দেখাবে। person@example.com দিলে email error সরে যাওয়ার কথা। তৃতীয় test-এ subject ফাঁকা রাখুন, message-এ ১৯টি অক্ষর দিন; message-এর minimum constraint আলাদা করে কাজ করবে। অনেক বড় text paste করে maxlength-এর আচরণও দেখুন।
চতুর্থ test হল keyboard-only: mouse না নিয়ে Tab দিয়ে label-এর পর field-এ যান, Enter/Space-এ submit করুন। কমলা focus-visible outline দেখা উচিত এবং invalid হলে প্রথম ভুল field focus পাওয়া উচিত। পঞ্চম test-এ browser DevTools থেকে JavaScript নিষ্ক্রিয় করে page reload করুন। Script-এর বাংলা custom error ও demo success থাকবে না, কিন্তু required, email, minlength-এর native validation চলবে। এই fallback-এ browser-এর native constraints দেখা যায়, কিন্তু demo button ইচ্ছাকৃতভাবে disabled থাকায় JavaScript ছাড়া submission শুরু হয় না; ফলে network-এ কিছু পাঠানোর দাবিও নেই।
ষষ্ঠ test-এ screen reader বা browser accessibility tree দিয়ে status ও error পড়ে দেখুন। Error text কেবল color নয় এবং label/hint/error সম্পর্কিত কিনা যাচাই করুন। সপ্তম test-এ forced-colors mode এবং zoom 200% চালু করুন; content কাটা বা focus হারানো উচিত নয়। সব test সফল হলেও এটি security test নয়—network, authorization, abuse prevention ও data retention আলাদা যাচাই।
Production-এ গেলে server validation অপরিহার্য
এই page-এ client-side validation user experience-এর জন্য, বিশ্বাসের জন্য নয়। Production endpoint-এ server অবশ্যই request body parse করে নাম, email, subject ও message-এর type, length, encoding এবং business rule যাচাই করবে। Unknown fields বাদ দিন, canonicalize করার সময় data নষ্ট না করে নীতিমালা লিখুন, এবং error response-এ sensitive details ফাঁস করবেন না। Server-side validation client rule-এর সঙ্গে সামঞ্জস্যপূর্ণ হতে পারে, কিন্তু client code কপি করে নিরাপত্তা সম্পন্ন হয়েছে ভাবা যাবে না।
বাস্তব POST form-এ CSRF প্রতিরোধ আলাদা বিষয়। Cookie-based session থাকলে server-side CSRF token তৈরি করে form-এ যুক্ত করুন এবং server-এ token মিলিয়ে দেখুন; SameSite cookie, Origin/Referer যাচাই এবং framework-এর vetted CSRF middleware সহায়ক প্রতিরক্ষা। এই demo-তে token নেই, কারণ backend নেই—তাই এটিকে authenticated production form হিসেবে ব্যবহার করবেন না। এই demo-র method="dialog" কোনো CSRF protection নয়; বাস্তব endpoint যোগ করলে action, method, CSRF ও privacy policy নতুন করে নির্ধারণ করতে হবে।
Contact form সাধারণত spam target। Rate limit, IP বা account ভিত্তিক quota, exponential backoff, captcha/turnstile-এর মতো risk-based challenge এবং abuse monitoring দরকার হতে পারে। Rate limit server বা edge-এ প্রয়োগ করুন; JavaScript-এ button একবার disable করলেই attacker থামে না। Message email-এ forward করলে header injection, HTML escaping, attachment policy এবং outbound provider-এর failure handling ভাবুন। সফল response তখনই বলবেন যখন server সত্যিই গ্রহণ করেছে; এই demo-র “NOT sent” ভাষা সেই distinction শেখায়।
Privacy, retention এবং error আচরণ
Form-এ email ও message ব্যক্তিগত তথ্য হতে পারে। Production-এ কেন তথ্য নিচ্ছেন, কতদিন রাখবেন, কে দেখবে, delete/access request কীভাবে হবে—সংক্ষিপ্ত privacy notice-এ বলুন। Consent checkbox দরকার হলে তা optional marketing consent-এর সঙ্গে বাধ্যতামূলক contact request মেশাবেন না। TLS ছাড়া sensitive form পাঠাবেন না; logs-এ সম্পূর্ণ message বা email না রাখাই ভালো, আর analytics tool যেন field value capture না করে তা যাচাই করুন।
Error response-এ “email already exists” বা account enumeration-এর মতো তথ্য প্রকাশের আগে threat model করুন। Accessibility-এর জন্য error message স্পষ্ট, সংক্ষিপ্ত এবং করণীয়মুখী রাখুন; “invalid” বলার বদলে “একটি বৈধ email format দিন” ভালো। Server 400, 429 বা 500 দিলে UI আলাদা state দেখাবে—network failure-কে validation error বলবে না। Retry-এর আগে user-এর entered value অক্ষুণ্ণ রাখুন, তবে shared/public device-এ sensitive data ধরে রাখার trade-off ব্যাখ্যা করুন।
Demo থেকে বাস্তব integration-এর checklist
action-এ HTTPS endpoint দিন এবংmethod="post"রাখুন; GET-এ message পাঠাবেন না। ২. Server-side schema validation, CSRF token, authentication/authorization এবং rate limit যোগ করুন। ৩. Success message server-এর authoritative response থেকে বানান, front-end অনুমান থেকে নয়। ৪. Error payload-এ field-level safe messages দিন এবং unknown exception user-কে দেখাবেন না। ৫. Privacy notice, retention deletion এবং log redaction লিখিত policy-তে আনুন। ৬. Keyboard, screen reader, zoom, forced-colors, slow network এবং JavaScript-off test চালান। ৭.fetchব্যবহার করলে timeout, abort, retry policy ও duplicate submission সামলান—এই file-এ সেগুলো ইচ্ছাকৃতভাবে নেই।
শেষ কথা: একটি ভালো form-এর মূল্য submit button-এর gradient-এ নয়; user কী চাইছে বুঝতে পারছেন কিনা, ভুল হলে কীভাবে ফেরত আসবেন, keyboard-এ সব reachable কিনা, এবং data কোথায় যাচ্ছে তা সত্যি বলা হচ্ছে কিনা—এসবের সমষ্টিতে। এই standalone demo native HTML দিয়ে শুরু করে CSS focus feedback ও JavaScript status যোগ করেছে, কিন্তু কোনো fake backend বা misleading success বানায়নি। সেই সীমা বজায় রেখেই পরের ধাপে নিজের server contract যোগ করুন।
No comments:
Post a Comment