پرتوپرتو

حالت‌های تعامل

فوکوس، غیرفعال و hover — ایدیوم واقعی سیستم، و توکن‌هایی که هیچ‌کس نمی‌خواندشان

اصل

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


نمونه بصری

کلید Tab را بزنید و بین این عنصرها حرکت کنید. حلقهٔ فوکوس فقط با صفحه‌کلید ظاهر می‌شود — با ماوس روی همین‌ها کلیک کنید و ببینید که نمی‌آید. همین تفاوت، دلیل وجود focus-visible است.

حلقهٔ استاندارد — کنترل‌ها

دکمهٔ غیرفعال از ترتیب Tab بیرون است — این کارِ صفتِ disabled است، نه کم‌رنگ‌کردن با کلاس.

حلقهٔ تودرتو — ردیف و آیتم فهرست (inset-focus)

رونمایی محصول جدید
کمپین تخفیف فصلی
بازخورد بسته‌بندی

حلقه با outline-offset: -2px داخل ردیف کشیده می‌شود. اگر بیرون بود، با حاشیهٔ ردیف بعدی دو خط چسبیده می‌ساخت.

تفاوتی که دیده نمی‌شود مگر با صفحه‌کلید

روی دومی با ماوس کلیک کنید: حلقه می‌ماند. همین نویز است که بعداً کسی را وسوسه می‌کند حلقه را کلاً بردارد.


قوانین اجباری

حلقهٔ فوکوس: focus-visible، نه focus

// ✅ درست — فقط برای ناوبری با صفحه‌کلید دیده می‌شود
<button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand">
  ذخیره
</button>

// ❌ غلط — با کلیک ماوس هم حلقه می‌گذارد، که نویز بصری است
<button className="focus:ring-2 focus:ring-brand">ذخیره</button>

// ❌ غلط‌تر — صفحه را برای کاربر صفحه‌کلید ناوبری‌ناپذیر می‌کند
<button className="outline-none">ذخیره</button>

focus-visible را مرورگر فقط وقتی فعال می‌کند که کاربر با صفحه‌کلید رسیده باشد. این تفاوت، دلیل وجود آن است.

برای ردیف و آیتم فهرست، inset-focus

<TableRow className="inset-focus" tabIndex={0}></TableRow>

.inset-focus حلقه را با outline-offset: -2px داخل عنصر می‌کشد. دلیلش مشخص است: حلقهٔ بیرونی روی ردیف جدول با حاشیهٔ ردیف بعدی تداخل می‌کند و دو خط چسبیده می‌سازد. این کلاس opt-in است و روی ردیف غیرقابل‌کلیک گذاشته نمی‌شود.

`--focus-ring-color` و `--disabled-opacity` را هیچ‌کس نمی‌خواند

هر دوی این‌ها در globals.css اعلام شده‌اند — یک‌بار برای هر تم — و هیچ کامپوننتی آن‌ها را نمی‌خواند. رنگ فوکوس در عمل از کلاس ring-brand می‌آید و .inset-focus هم مستقیماً var(--brand-600) را می‌نویسد. پس رویشان حساب نکنید: عوض‌کردنشان هیچ اثری ندارد. تا وقتی تکلیفشان روشن شود، ایدیومِ واقعی را به کار ببرید.

غیرفعال: هم بصری، هم معنایی

// ✅ درست — صفحه‌خوان هم می‌فهمد
<Button disabled>ارسال</Button>

// ❌ غلط — فقط کم‌رنگ است؛ همچنان قابل فوکوس و کلیک است
<Button className="opacity-50">ارسال</Button>

اگر عنصر واقعاً <button disabled> نیست (مثلاً یک div نقش‌دار)، aria-disabled="true" بگذارید و خودتان اکشن را ببندید.


موارد استفاده رایج

ترتیب حالت‌ها

وقتی چند حالت هم‌زمان ممکن است، ترتیب اهمیت این است: disabled → focus-visible → active → hover. یعنی یک دکمهٔ غیرفعال نباید hover نشان دهد، و یک دکمهٔ فوکوس‌دار باید حلقه‌اش را حتی زیر hover نگه دارد.

hover فقط جایی که ماوس هست

// روی صفحهٔ لمسی، hover می‌تواند «بچسبد»
<div className="hover:bg-surface-200"></div>

برای حالت‌هایی که روی موبایل گیج‌کننده‌اند، از @media (hover: hover) یا واریانت hover تِیلویند همراه با تست روی دستگاه لمسی استفاده کنید.


چه نکنیم

  • برداشتن outline بدون جایگزین. رایج‌ترین نقص دسترسی‌پذیری در کد محصول.
  • focus به‌جای focus-visible. حلقه روی کلیک ماوس هم ظاهر می‌شود و طراحی را شلوغ نشان می‌دهد، بعد کسی وسوسه می‌شود کلاً حذفش کند.
  • کم‌رنگ‌کردن به‌جای disabled. عنصر همچنان در ترتیب Tab هست و کلیک می‌خورد.
  • تکیه بر --focus-ring-color یا --disabled-opacity. هیچ‌کدام خوانده نمی‌شوند.

صفحات مرتبط