Tailwind CSS Academy · درس

مكوّنات النماذج سهلة الوصول

ضع تسمية لكل حقل إدخال، واربط الأخطاء باستخدام aria-describedby، ووفّر مؤشرات واضحة للحقول المطلوبة ورسائل خطأ وصفية.

الدرس 4 من 413 خطوة

مكوّنات النماذج سهلة الوصول درس مجاني في Tailwind CSS Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Tailwind CSS Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Tailwind CSS Academy 4 دروس في المجموع.

بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.

Why Accessible Forms Matter

Forms are one of the most critical interaction points in web applications — used for login, checkout, search, and data entry. Inaccessible forms exclude users who rely on screen readers, keyboard navigation, or voice control. Common failures include unlabeled inputs, errors that only appear visually, required field indicators that screen readers cannot detect, and focus that does not move to errors after submission. Tailwind provides all the utilities needed to build forms that work for everyone.

Labeling Every Input

Every form input must have a programmatic label — not just visual placeholder text. Placeholders disappear when the user types and are not reliably read by all screen readers. Use a <label> element with a for attribute matching the input's id. This creates a binding: clicking the label focuses the input, and screen readers announce the label when the input is focused. Never remove visible labels for design reasons — they are accessibility requirements.

<!-- GOOD: visible label with for/id binding -->
<div class='flex flex-col gap-1'>
  <label for='email' class='text-sm font-medium text-gray-700'>
    Email address
  </label>
  <input
    id='email'
    type='email'
    name='email'
    placeholder='you@example.com'
    class='rounded-lg border border-gray-300 px-3 py-2 text-sm
           focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500'
  />
</div>

<!-- BAD: placeholder-only labeling -->
<input type='email' placeholder='Email address'
  class='rounded-lg border border-gray-300 px-3 py-2' />
{/* Placeholder disappears on input — user forgets what the field is for */}

Required Field Indicators

Required fields need to be indicated both visually and programmatically. Add the required HTML attribute (screen readers announce it) and a visual indicator — typically an asterisk — with a legend explaining what the asterisk means. Use aria-required='true' for custom form elements that do not support the native required attribute. Never rely solely on color to indicate required status.

<form>
  {/* Explain the asterisk at the top of the form */}
  <p class='text-sm text-gray-500 mb-6'>
    Fields marked with
    <span class='text-red-600 font-bold' aria-hidden='true'> *</span>
    <span class='sr-only'>an asterisk</span>
    are required.
  </p>

  <div class='flex flex-col gap-1'>
    <label for='name' class='text-sm font-medium text-gray-700'>
      Full name
      <span class='text-red-600 ml-0.5' aria-hidden='true'>*</span>
    </label>
    <input
      id='name'
      type='text'
      required
      aria-required='true'
      class='rounded-lg border border-gray-300 px-3 py-2'
    />
  </div>
</form>

Error Messages With aria-describedby

When a field has a validation error, the error message must be programmatically associated with the input. Use aria-describedby on the input referencing the error message element's ID. Screen readers will announce the error message after announcing the field label. Also set aria-invalid='true' on the input when it has an error — this signals to assistive technology that the field value is invalid.

function FormField({ id, label, error, ...inputProps }) {
  const errorId = id + '-error';

  return (
    <div class='flex flex-col gap-1'>
      <label for={id} class='text-sm font-medium text-gray-700'>
        {label}
      </label>
      <input
        id={id}
        aria-invalid={error ? 'true' : undefined}
        aria-describedby={error ? errorId : undefined}
        class={cn(
          'rounded-lg border px-3 py-2 text-sm',
          'focus:outline-none focus-visible:ring-2',
          error
            ? 'border-red-400 focus-visible:ring-red-500'
            : 'border-gray-300 focus-visible:ring-blue-500'
        )}
        {...inputProps}
      />
      {error && (
        <p id={errorId} class='text-sm text-red-600 flex items-center gap-1'>
          <ExclamationCircleIcon class='h-4 w-4 flex-shrink-0' aria-hidden='true' />
          {error}
        </p>
      )}
    </div>
  );
}

Focus Management After Form Submission

When a form is submitted and validation errors are found, move focus to the first error or to an error summary at the top of the form. Users who are tabbing through the form will otherwise have no indication that submission failed — the error messages might appear below the fold or in areas they have already passed. Moving focus to the error summary is the most robust pattern as it works regardless of where errors appear.

function ContactForm() {
  const [errors, setErrors] = useState({});
  const errorSummaryRef = useRef(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const validation = validateForm(formData);

    if (Object.keys(validation).length > 0) {
      setErrors(validation);
      // Move focus to error summary after state update
      setTimeout(() => errorSummaryRef.current?.focus(), 0);
      return;
    }
    // ... submit
  };

  return (
    <form onSubmit={handleSubmit}>
      {Object.keys(errors).length > 0 && (
        <div
          ref={errorSummaryRef}
          tabIndex={-1}
          role='alert'
          class='bg-red-50 border border-red-200 rounded-lg p-4 mb-6 focus:outline-none'
        >
          <h2 class='text-sm font-semibold text-red-800 mb-2'>
            Please fix the following {Object.keys(errors).length} error(s):
          </h2>
          <ul class='list-disc list-inside text-sm text-red-700'>
            {Object.entries(errors).map(([field, msg]) => (
              <li key={field}><a href={'#' + field} class='underline'>{msg}</a></li>
            ))}
          </ul>
        </div>
      )}
      {/* form fields */}
    </form>
  );
}

Accessible Checkbox and Radio Groups

Checkboxes and radio buttons should be grouped in a <fieldset> with a <legend> that describes the group. The legend is announced by screen readers when a group member receives focus, providing essential context. Each individual checkbox or radio still needs its own <label>. Without the fieldset/legend grouping, screen reader users hear the individual label but miss the group context.

<fieldset class='border-0 p-0 m-0'>
  <legend class='text-sm font-semibold text-gray-800 mb-3'>
    Notification preferences
  </legend>

  <div class='flex flex-col gap-3'>
    {['Email', 'SMS', 'Push'].map(option => (
      <label key={option} class='flex items-center gap-3 cursor-pointer'>
        <input
          type='checkbox'
          name='notifications'
          value={option.toLowerCase()}
          class='
            h-4 w-4 rounded border-gray-300 text-blue-600
            focus-visible:ring-2 focus-visible:ring-blue-500
          '
        />
        <span class='text-sm text-gray-700'>{option} notifications</span>
      </label>
    ))}
  </div>
</fieldset>

Accessible Select and Combobox

Native <select> elements are accessible out of the box but limited in styling. When you need a custom-styled select with images or complex option layouts, use Headless UI's Listbox or Combobox components which implement the ARIA listbox pattern. Always include a visible <label> associated with the select. Never use aria-label alone — visible labels help all users, not just screen reader users.

<!-- Native select: accessible, limited styling -->
<div class='flex flex-col gap-1'>
  <label for='country' class='text-sm font-medium text-gray-700'>
    Country
  </label>
  <select
    id='country'
    name='country'
    class='
      rounded-lg border border-gray-300 px-3 py-2 text-sm bg-white
      focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500
    '
  >
    <option value=''>Select a country</option>
    <option value='us'>United States</option>
    <option value='uk'>United Kingdom</option>
    <option value='ca'>Canada</option>
  </select>
</div>

Password Fields and Visibility Toggles

Password fields with show/hide toggles need careful accessibility implementation. The toggle button should have an aria-label that clearly describes its current action ('Show password' or 'Hide password'). When toggled, use aria-live to announce the state change to screen reader users. The password input type should change between password and text — do not use a custom masking approach.

function PasswordInput({ id, label }) {
  const [visible, setVisible] = useState(false);
  const announcement = visible ? 'Password is now visible' : 'Password is now hidden';
  const [liveText, setLiveText] = useState('');

  const toggle = () => {
    setVisible(!visible);
    setLiveText(announcement);
  };

  return (
    <div class='flex flex-col gap-1'>
      <label for={id} class='text-sm font-medium text-gray-700'>{label}</label>
      <div class='relative'>
        <input
          id={id}
          type={visible ? 'text' : 'password'}
          class='w-full rounded-lg border border-gray-300 px-3 py-2 pr-10 text-sm
                 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500'
        />
        <button
          type='button'
          onClick={toggle}
          aria-label={visible ? 'Hide password' : 'Show password'}
          class='absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600'
        >
          {visible ? <EyeSlashIcon class='h-4 w-4' /> : <EyeIcon class='h-4 w-4' />}
        </button>
      </div>
      <span class='sr-only' aria-live='polite'>{liveText}</span>
    </div>
  );
}

Inline Form Validation Feedback

Provide validation feedback in real time as users complete fields (not only on submission) to catch errors early. Use onBlur to trigger validation when a field loses focus — not on every keystroke, which is annoying. The feedback should be clear: a green checkmark or success text for valid fields, a red error message for invalid ones. Always communicate both the error and how to fix it.

function ValidatedInput({ id, label, validate }) {
  const [value, setValue] = useState('');
  const [error, setError] = useState('');
  const [touched, setTouched] = useState(false);
  const isValid = touched && !error && value;

  const handleBlur = () => {
    setTouched(true);
    const err = validate(value);
    setError(err || '');
  };

  return (
    <div class='flex flex-col gap-1'>
      <label for={id} class='text-sm font-medium text-gray-700'>{label}</label>
      <div class='relative'>
        <input
          id={id}
          value={value}
          onChange={e => setValue(e.target.value)}
          onBlur={handleBlur}
          aria-invalid={touched && error ? 'true' : undefined}
          aria-describedby={error ? id + '-error' : undefined}
          class={cn('w-full rounded-lg border px-3 py-2 pr-8 text-sm',
            error && touched ? 'border-red-400' : isValid ? 'border-green-500' : 'border-gray-300'
          )}
        />
        {isValid && <CheckCircleIcon class='absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500' />}
      </div>
      {error && touched && (
        <p id={id + '-error'} class='text-sm text-red-600'>{error}</p>
      )}
    </div>
  );
}

Full Accessible Form Example

A fully accessible form combines: associated labels, required field indicators with aria-required, error messages with aria-describedby, error summary with focus management, appropriate input types, and fieldset/legend for groups. The form should be operable entirely via keyboard, and all information should be available to screen readers. Tailwind handles the visual layer; HTML semantics and ARIA handle the accessibility layer.

<form onSubmit={handleSubmit} noValidate>
  {/* Error summary */}
  {hasErrors && (
    <div ref={summaryRef} tabIndex={-1} role='alert'
      class='bg-red-50 border border-red-200 rounded-lg p-4 mb-6'>
      <h2 class='text-sm font-semibold text-red-800'>Fix these errors:</h2>
      {/* error list with anchor links to fields */}
    </div>
  )}

  {/* Name field */}
  <div class='flex flex-col gap-1 mb-4'>
    <label for='name' class='text-sm font-medium'>
      Name <span aria-hidden='true' class='text-red-600'>*</span>
    </label>
    <input id='name' required aria-required='true'
      aria-invalid={errors.name ? 'true' : undefined}
      aria-describedby={errors.name ? 'name-error' : undefined}
      class='rounded-lg border border-gray-300 px-3 py-2 focus-visible:ring-2 focus-visible:ring-blue-500' />
    {errors.name && <p id='name-error' class='text-sm text-red-600'>{errors.name}</p>}
  </div>

  <button type='submit' class='w-full bg-blue-600 text-white rounded-lg py-2 font-medium hover:bg-blue-700'>Submit</button>
</form>

Testing Accessible Forms

Test accessible forms with three approaches. Automated: run axe-core on the rendered form. Keyboard: tab through every field, submit with errors, verify focus moves to error summary, fix errors, resubmit. Screen reader: test with VoiceOver (macOS/iOS) and NVDA (Windows), listening for label announcements, error announcements, and state changes. Each testing method catches a different class of issues.

// Automated: test with jest-axe
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import ContactForm from './ContactForm';

test('Contact form has no accessibility violations', async () => {
  const { container } = render(<ContactForm />);
  expect(await axe(container)).toHaveNoViolations();
});

test('Error messages are associated with inputs', async () => {
  const { container, getByRole } = render(<ContactForm />);
  // Submit empty form to trigger errors
  fireEvent.click(getByRole('button', { name: /submit/i }));
  // axe checks aria-describedby associations
  expect(await axe(container)).toHaveNoViolations();
});

Quick Check

Test your understanding of Tailwind CSS Mastery concepts from this lesson.

Lesson Recap

In this lesson you learned: every input needs a visible label with a for/id association, aria-invalid and aria-describedby connect error messages to their inputs programmatically, and focus management moves users to error summaries after failed submissions. Congratulations on completing the Accessible Components module — you now have a comprehensive toolkit for building inclusive Tailwind interfaces.

البدء مجانًا

تعلم HTML مع معلم ذكاء اصطناعي — مجانًا

اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.

الدورات
30
الدروس
120

الأسئلة الشائعة

هل درس «مكوّنات النماذج سهلة الوصول» مجاني؟

نعم — نص درس «مكوّنات النماذج سهلة الوصول» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Tailwind CSS Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Tailwind CSS Academy 4 دروس في المجموع.

ماذا ستتعلم في «مكوّنات النماذج سهلة الوصول»؟

ضع تسمية لكل حقل إدخال، واربط الأخطاء باستخدام aria-describedby، ووفّر مؤشرات واضحة للحقول المطلوبة ورسائل خطأ وصفية. تتمرن على Tailwind CSS Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Tailwind CSS Academy؟

لا تُشترط خبرة سابقة. Tailwind CSS Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «مكوّنات النماذج سهلة الوصول»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Tailwind CSS Academy هذا؟

نعم. كل درس في Tailwind CSS Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. تباين الألوان والنص المقروء
  2. مؤشرات التركيز والتنقل بلوحة المفاتيح
  3. سمات ARIA وقارئات الشاشة
  4. مكوّنات النماذج سهلة الوصول
← العودة إلى Tailwind CSS Academy