Label و دسترس‌پذیری فرم‌ها

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

مشکل: یک متن که فقط از نظر بصری کنار فیلد است

jsx
// نامناسب از نظر دسترس‌پذیری
function EmailField() {
  return (
    <div>
      <p>ایمیل</p>
      <input type="email" />
    </div>
  );
}

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

راه‌حل: تگ label با htmlFor

jsx
function EmailField() {
  return (
    <div>
      <label htmlFor="email">ایمیل</label>
      <input id="email" type="email" />
    </div>
  );
}

در JSX، چون for یک کلمه رزروشده در جاوااسکریپت است (شبیه ماجرای class در فصل اول)، از htmlFor استفاده می‌شود. مقدار htmlFor باید دقیقاً برابر با id همان فیلد باشد؛ این دو مقدار، پیوند رسمی بین برچسب و فیلد را می‌سازند.

دو مزیت این پیوند رسمی

با این اتصال، دو چیز به‌طور خودکار اتفاق می‌افتد: صفحه‌خوان، هنگام فوکوس روی فیلد، متن Label را هم اعلام می‌کند؛ و کلیک روی خود متن Label، فوکوس را به فیلد منتقل می‌کند (مفید برای چک‌باکس‌های کوچک که کلیک دقیق روی خودشان سخت است).

راه‌حل جایگزین: قرار دادن input داخل label

یک روش دیگر، تودرتو کردن خود فیلد داخل label است؛ در این حالت، نیازی به htmlFor و id جداگانه نیست، چون تودرتویی خودش رابطه را مشخص می‌کند:

jsx
function AgreeCheckbox() {
  return (
    <label>
      <input type="checkbox" />
      شرایط را می‌پذیرم
    </label>
  );
}

وقتی Label بصری وجود ندارد: aria-label

گاهی، به دلایل طراحی، نمی‌خواهیم متن برچسب روی صفحه دیده شود (مثلاً یک فیلد جست‌وجو که فقط یک آیکون ذره‌بین دارد). در این موارد، نباید Label را کامل حذف کرد؛ به‌جای آن، aria-label یک متن فقط برای ابزارهای کمکی فراهم می‌کند، بدون اینکه چیزی از نظر بصری اضافه شود:

jsx
<input type="search" aria-label="جست‌وجو در محصولات" />

پیام خطا هم باید به فیلد متصل شود

همان‌طور که در فصل چهارم درباره اعتبارسنجی فرم دیدیم، وقتی یک فیلد پیام خطا دارد، آن پیام هم باید رسماً به فیلد وصل شود؛ معمولاً با aria-describedby:

jsx
<input id="email" aria-describedby="email-error" />
<p id="email-error">ایمیل معتبر نیست</p>

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

مثال قابل اجرا

فرم با Label و پیام خطای متصل به‌درستی
import { useState } from "react";

function App() {
  const [email, setEmail] = useState("");
  const isValid = email.includes("@");

  return (
    <form>
      <label htmlFor="email">ایمیل</label>
      <input
        id="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        aria-describedby="email-error"
      />
      {!isValid && email.length > 0 && (
        <p id="email-error">ایمیل معتبر نیست</p>
      )}
    </form>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

یک فیلد فرم باید همیشه با یک Label، از طریق htmlFor/id یا تودرتویی مستقیم، رسماً مرتبط باشد؛ وقتی Label بصری امکان‌پذیر نیست، aria-label جایگزین می‌شود، و پیام خطا باید با aria-describedby به فیلد وصل شود. بدون این اتصال‌های رسمی، ابزارهای کمکی فقط یک فیلد بی‌نام و نشان می‌بینند. در درس بعدی، به دسترس‌پذیری با صفحه‌کلید می‌پردازیم.