دسترس‌پذیری در فرم‌ها؛ جایی که CSS فقط نصف ماجراست

تو سه درس قبل، دیدیم چطور فرم‌ها رو زیبا کنیم، چک‌باکس و رادیو بسازیم، و به کاربر بازخورد فوکوس و اعتبارسنجی بدیم. اما تو خیلی از این درس‌ها، بارها به دسترس‌پذیری اشاره کردیم بدون این‌که کامل روش تمرکز کنیم. حالا وقتشه یه درس مستقل بهش اختصاص بدیم، چون فرم‌ها دقیقاً همون جایی هستن که بی‌توجهی به accessibility، بیشترین آسیب رو به کاربرهای واقعی می‌زنه.

چرا فرم‌ها اهمیت ویژه‌ای دارن؟

یه دکمه‌ی دسترس‌ناپذیر شاید فقط باعث بشه کاربر نتونه یه کار رو انجام بده. اما یه فرم دسترس‌ناپذیر (مثلاً فرم ثبت‌نام، پرداخت، یا تماس با ما) می‌تونه کاربر رو کاملاً از یه فرآیند حیاتی حذف کنه. به همین دلیل، فرم‌ها معمولاً اولین جایی هستن که تو ممیزی‌های accessibility بررسی می‌شن.

label: اولین و مهم‌ترین قدم

هر input باید یه label مرتبط داشته باشه. این ربطی به CSS نداره، ولی چون مستقیم روی استایل‌دهی هم اثر می‌ذاره (مثل مثال‌های :has() که تو درس‌های قبل دیدیم)، باید ساختارش رو بشناسید:

<label for="email">ایمیل</label>
<input type="email" id="email">

<!-- یا به‌صورت تو در تو، بدون نیاز به for/id -->
<label>
  ایمیل
  <input type="email">
</label>

بدون این ارتباط، صفحه‌خوان‌ها نمی‌تونن به کاربر بگن این input برای چیه؛ و از نظر CSS هم، خیلی از selector های کاربردی مثل همون label:has(+ input:required) که تو درس قبل دیدیم، اصلاً کار نمی‌کنن اگه ساختار label درست نباشه.

هیچ‌وقت placeholder رو جایگزین label نکنید

یه اشتباه خیلی رایج (که متاسفانه از نظر بصری هم وسوسه‌انگیزه) اینه که به‌جای label، فقط از placeholder استفاده کنید:

/* از نظر ظاهری قشنگه، ولی از نظر accessibility مشکل داره */
<input type="text" placeholder="نام کامل">

مشکل اینجاست: به محض این‌که کاربر شروع به تایپ می‌کنه، placeholder ناپدید می‌شه و دیگه هیچ نشونه‌ای از این‌که این فیلد برای چیه باقی نمی‌مونه؛ نه از نظر بصری، نه برای صفحه‌خوان (چون خیلی از صفحه‌خوان‌ها placeholder رو به‌اندازه‌ی label قوی اعلام نمی‌کنن). راه‌حل درست، همیشه داشتن یه label واقعیه، حتی اگه بخواید ظاهرش رو مخفی کنید.

وقتی واقعاً نمی‌خواید label به‌صورت بصری دیده بشه

گاهی طراحی ایجاب می‌کنه که label به‌صورت بصری نباشه (مثلاً یه فیلد جستجوی تنها با یه آیکون). تو این حالت‌ها، به‌جای حذف کامل label، از یه کلاس «visually hidden» استفاده کنید:

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

این کلاس، المنت رو از نظر بصری کاملاً جمع می‌کنه به یه پیکسل، ولی همچنان تو DOM و در دسترس صفحه‌خوان باقی می‌مونه. این دقیقاً برعکس display: none عمل می‌کنه، که هم از نظر بصری هم از نظر صفحه‌خوان، المنت رو کاملاً حذف می‌کنه.

رنگ به‌تنهایی کافی نیست

تو درس‌های قبل، برای نشون دادن خطا معمولاً border رو قرمز کردیم:

input:invalid:not(:placeholder-shown) {
  border-color: #dc2626;
}

این به‌تنهایی برای کاربرهای کوررنگ یا کسایی که دید ضعیفی دارن کافی نیست، چون ممکنه فرق قرمز و مقدار عادی border رو تشخیص ندن. همیشه باید یه نشونه‌ی غیر‌رنگی هم اضافه کنید؛ مثلاً یه آیکون، یه متن خطا، یا حتی تغییر ضخامت border:

input:invalid:not(:placeholder-shown) {
  border-color: #dc2626;
  border-width: 2px;
  background-image: url("data:image/svg+xml,...error-icon...");
  background-repeat: no-repeat;
  background-position: right 10px center;
}

اندازه‌ی هدف کلیک (Touch Target Size)

این یه نکته‌ی CSS خالصه که خیلی وقت‌ها نادیده گرفته می‌شه: checkbox، radio و دکمه‌های کوچیک، باید حداقل یه اندازه‌ی مشخص (معمولاً حداقل ۲۴ تا ۴۴ پیکسل بسته به استاندارد) داشته باشن تا کاربرهایی که مشکل حرکتی دارن یا با موبایل کار می‌کنن، بتونن راحت لمسشون کنن:

input[type="checkbox"],
input[type="radio"] {
  width: 20px;
  height: 20px;
}

/* بهتر: بزرگ کردن ناحیه‌ی قابل‌کلیک با padding روی label، نه فقط خود input */
label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
}

حتی اگه خود دایره‌ی چک‌باکس کوچیک بمونه، با گذاشتن padding روی label (که با for بهش وصله)، کل ناحیه‌ی قابل‌کلیک بزرگ‌تر می‌شه، بدون این‌که ظاهر خود چک‌باکس عوض بشه.

fieldset و legend برای گروه‌بندی

وقتی چند radio یا checkbox مرتبط با هم دارید (مثل یه گروه سؤال چندگزینه‌ای)، باید با fieldset و legend گروه‌بندی بشن، نه فقط با یه div ساده:

<fieldset>
  <legend>روش ارسال</legend>
  <label><input type="radio" name="shipping"> پست عادی</label>
  <label><input type="radio" name="shipping"> پست پیشتاز</label>
</fieldset>

از نظر CSS، fieldset یه المنت عادیه و می‌شه هر استایلی که خواستید بهش بدید (چون استایل پیش‌فرضش با border و padding عجیب معمولاً نیاز به ریست داره)، ولی از نظر ساختاری، این تگ به صفحه‌خوان می‌گه «این چند تا کنترل، یه گروه واحد هستن با این عنوان».

نکته مهم: ریست کردن استایل fieldset معمولاً لازمه چون مرورگرها پیش‌فرض عجیبی (border خاکستری، padding نامتقارن) براش دارن:
fieldset {
  border: none;
  padding: 0;
  margin: 0 0 16px;
}

legend {
  font-weight: bold;
  margin-bottom: 8px;
}

اشتباه رایج: قربانی کردن دسترس‌پذیری برای ظاهر «تمیزتر»

یه الگوی رایج و خطرناک اینه که برای رسیدن به یه طراحی «مینیمال‌تر»، label ها حذف بشن، outline فوکوس برداشته بشه، یا اندازه‌ی هدف کلیک به‌خاطر ظاهر ریزتر کوچیک بشه. همه‌ی این‌ها تصمیم‌های ظاهری هستن که مستقیماً یه بخش از کاربرها رو از استفاده‌ی راحت فرم محروم می‌کنن. یه فرم خوب، همیشه باید دسترس‌پذیری رو به‌عنوان یه محدودیت طراحی از همون ابتدا در نظر بگیره، نه یه چیزی که بعداً اضافه می‌شه.

جمع‌بندی

دسترس‌پذیری فرم فقط یه لایه‌ی CSS نیست؛ از ساختار HTML درست (label، fieldset، legend) شروع می‌شه و با تصمیم‌های CSS مثل نگه‌داشتن outline فوکوس، بزرگ نگه‌داشتن ناحیه‌ی قابل‌کلیک، و ندادن اتکای کامل به رنگ برای نشون‌دادن خطا کامل می‌شه. هیچ‌وقت placeholder رو جایگزین label نکنید، و اگه لازمه label رو مخفی کنید، از تکنیک visually-hidden استفاده کنید، نه display: none.