چک‌باکس و رادیو باتن؛ عمیق‌تر از appearance: none

تو درس قبل، یه نگاه کلی به استایل‌دهی فرم‌ها انداختیم و یه مثال ساده از استایل‌دهی چک‌باکس با appearance: none دیدیم. اما چک‌باکس و رادیو باتن آن‌قدر ریزه‌کاری دارن که ارزش یه درس مستقل رو دارن. تو این درس عمیق‌تر می‌ریم: فرق واقعی رادیو با چک‌باکس، حالت indeterminate، و یه روش جدیدتر و تمیزتر برای استایل‌دهی که با accent-color شروع می‌شه.

ساده‌ترین راه: accent-color

قبل از این‌که سراغ ساخت کامل از صفر بریم، یه راه خیلی ساده‌تر هم هست که خیلی وقت‌ها کافیه: ویژگی accent-color. این ویژگی رنگ اصلی چک‌باکس، رادیو، range و چند کنترل دیگه رو عوض می‌کنه، بدون این‌که شکل native رو کاملاً حذف کنید:

input[type="checkbox"],
input[type="radio"] {
  accent-color: #3b82f6;
}

این روش شکل و رفتار پیش‌فرض سیستم‌عامل رو حفظ می‌کنه (که خودش یه مزیت accessibility هست، چون کاربر با ظاهر آشنای سیستم‌عاملش کار می‌کنه)، فقط رنگش رو با برند شما هماهنگ می‌کنه. برای خیلی از پروژه‌ها، همین یه خط کافیه و نیازی به appearance: none نیست.

فرق ساختاری radio با checkbox

از نظر ظاهری شبیه هم به نظر می‌رسن، ولی رفتارشون فرق اساسی داره: چند تا checkbox با هم مستقل عمل می‌کنن (هرکدوم جدا تیک می‌خوره)، ولی چند تا radio که name یکسان دارن، یه گروه رو تشکیل می‌دن که فقط یکیشون در آن واحد می‌تونه انتخاب بشه:

/* این سه‌تا radio چون name یکسان دارن، یه گروهن */
<input type="radio" name="plan" value="basic">
<input type="radio" name="plan" value="pro">
<input type="radio" name="plan" value="enterprise">

این تفاوت رفتاری، وقتی می‌خواید کنترل‌های سفارشی بسازید هم باید رعایت بشه؛ یعنی حتی وقتی ظاهرشون رو کاملاً از نو می‌سازید، خود input های اصلی باید همچنان با name یکسان گروه‌بندی بشن تا مرورگر منطق «فقط یکی انتخاب بشه» رو خودش مدیریت کنه.

ساخت کامل radio از صفر با appearance: none

برای کنترل کامل ظاهر، همون الگوی درس قبل رو با یه تفاوت کلیدی (border-radius دایره‌ای) پیاده می‌کنیم:

input[type="radio"] {
  appearance: none;
  width: 20px;
  height: 20px;
  border: 2px solid #999;
  border-radius: 50%;
  display: inline-grid;
  place-content: center;
}

input[type="radio"]::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 50%;
  transform: scale(0);
  transition: transform 0.15s ease-in-out;
  background: #3b82f6;
}

input[type="radio"]:checked::before {
  transform: scale(1);
}

نکته‌ی جالب اینجا استفاده از transform: scale() روی ::before به‌جای تغییر opacity یا اضافه/حذف کردن مستقیم رنگه؛ این‌جوری وقتی کاربر انتخابش رو عوض می‌کنه، نقطه‌ی وسط با یه انیمیشن نرم بزرگ می‌شه، نه این‌که یهو ظاهر بشه.

حالت indeterminate: نه تیک‌خورده، نه خالی

یه حالت سوم برای checkbox وجود داره که با HTML به‌تنهایی قابل تنظیم نیست و فقط با جاوااسکریپت فعال می‌شه: indeterminate. این حالت معمولاً برای یه checkbox «انتخاب همه» به کار می‌ره، وقتی بعضی (نه همه‌ی) آیتم‌های زیرمجموعه انتخاب شدن:

input[type="checkbox"]:indeterminate {
  background: #999;
  border-color: #999;
}

input[type="checkbox"]:indeterminate::before {
  content: "−";
  color: white;
}

چون این حالت فقط از طریق جاوااسکریپت (با checkbox.indeterminate = true) قابل فعال شدنه، CSS به‌تنهایی نمی‌تونه یه چک‌باکس رو indeterminate کنه؛ فقط می‌تونه بهش استایل بده وقتی از قبل با اسکریپت این حالت روش فعال شده.

استایل‌دهی به label مرتبط با checkbox/radio

یه الگوی خیلی رایج و مهم از نظر دسترس‌پذیری، اینه که کل کارت یا ردیف رو قابل‌کلیک کنید، نه فقط خود دایره‌ی کوچیک چک‌باکس. این کار با استفاده از selector :has() که تو فصل قبل باهاش آشنا شدیم، خیلی تمیز می‌شه:

.option-card:has(input:checked) {
  border-color: #3b82f6;
  background: #eff6ff;
}

این‌جوری، وقتی input داخل .option-card تیک بخوره، کل کارت (نه فقط checkbox) استایل «انتخاب‌شده» می‌گیره؛ که تجربه‌ی کاربری خیلی بهتری نسبت به فقط رنگ‌عوض‌کردن یه دایره‌ی کوچیک ایجاد می‌کنه.

نکته مهم: اگه checkbox یا radio سفارشی رو با appearance: none می‌سازید، هیچ‌وقت خود input رو کاملاً از دید مخفی نکنید (مثل display: none یا visibility: hidden)؛ چون این کار باعث می‌شه صفحه‌خوان‌ها (screen reader) و ناوبری کیبورد کاملاً نادیده‌اش بگیرن. اگه لازمه ظاهرش رو مخفی کنید ولی برای دسترس‌پذیری نگهش دارید، از تکنیک «visually hidden» (مثل کوچیک کردن به 1px با position: absolute و overflow: hidden) استفاده کنید، نه display: none.

اشتباه رایج: از دست دادن رفتار گروهی radio موقع سفارشی‌سازی

یه اشتباه رایج اینه که موقع ساختن رادیوباتن سفارشی، به‌جای استفاده از input های واقعی radio با name یکسان، از یه سری div یا span با کلاس‌های جاوااسکریپتی شبیه‌سازی می‌کنن. این کار هم منطق native انتخاب انحصاری رو از دست می‌ده، هم دسترس‌پذیری رو بدون تلاش زیاد و اضافه کردن ARIA attribute های دستی خراب می‌کنه. همیشه بهتره از خود input های واقعی استفاده کنید و فقط ظاهرشون رو با appearance: none تغییر بدید، نه این‌که کل عنصر رو با div شبیه‌سازی کنید.

جمع‌بندی

برای خیلی از پروژه‌ها، accent-color تنها چیزیه که لازم دارید و ظاهر native سیستم‌عامل رو با هزینه‌ی خیلی کم هماهنگ با برند می‌کنه. برای کنترل کامل‌تر، appearance: none همراه با ::before راه اصلیه، و باید حالت‌های :checked و :indeterminate رو جدا مدیریت کنید. مهم‌ترین اصل این درس اینه: همیشه از input های واقعی HTML استفاده کنید و فقط ظاهرشون رو عوض کنید، نه این‌که رفتارشون رو با div و جاوااسکریپت از صفر بازسازی کنید.