طراحی وب

طراحی Tab و Accordion با UX صحیح

انتخاب Tabs و Accordion بر اساس نیاز محتوا؛ activation دستی و خودکار، رابطه ARIA، fallback و نمونه مستقل Tabs و details.

1404/12/26 4 دقیقه مطالعه 567 بازدید

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

Tab معمولاً یک پنل را از میان چند پنل هم‌سطح نشان می‌دهد. Accordion امکان باز و بسته کردن بخش‌ها را می‌دهد و بسته به قرارداد می‌تواند چند بخش باز داشته باشد. این تفاوت فقط در ظاهر نیست؛ رفتار Keyboard، state و مسیر رسیدن به محتوا هم فرق دارند. نام‌گذاری چند دکمه به عنوان Tab بدون پیاده‌سازی این رفتارها، الگوی کامل نمی‌سازد.

پنهان کردن محتوا باید دلیل داشته باشد

در نمونه فرضی معرفی دوره، «سرفصل» و «پرسش‌های رایج» می‌توانند بخش‌های جدا باشند. اما هزینه و شرط لازم ثبت‌نام نباید فقط به دلیل کمبود جا در پنلی کم‌دیده پنهان شوند. این مثال داده یک مشتری واقعی نیست. از وظیفه کاربر بپرسید: آیا بین بخش‌ها جابه‌جا می‌شود یا نیاز دارد اطلاعات چند بخش را کنار هم بخواند؟

اگر مقایسه هم‌زمان مهم است، جدول یا بخش‌های باز شاید مناسب‌تر باشند. اگر هر بخش مستقل و طولانی است، صفحه جدا با navigation می‌تواند مسیر اشتراک و بازگشت روشن‌تری بدهد. Tab جایگزین عمومی navigation سایت نیست. انتخاب چند لینک به صفحه‌های متفاوت با الگوی Tabs معنایی، رفتار و انتظار نادرست ایجاد می‌کند.

حداکثر پنج یا هفت Tab قانون قطعی نیست. طول نام‌ها، عرض صفحه و رابطه محتوا تعیین‌کننده‌اند. تعداد زیاد می‌تواند یافتن را سخت کند، ولی کاهش تعداد با گروه‌بندی مبهم هم بهتر نیست. اگر نام‌ها برای جا شدن کوتاه و نامفهوم می‌شوند، ساختار اطلاعات را دوباره بررسی کنید. مشکل ممکن است از تقسیم محتوا باشد، نه از CSS دکمه‌ها.

Tab به رابطه برنامه‌ای با panel نیاز دارد

الگوی Tabs در WAI-ARIA APG رابطه tablist، tab و tabpanel و حالت انتخاب را توضیح می‌دهد. کنترل با aria-controls پنل مربوط را معرفی می‌کند و پنل با aria-labelledby به نام Tab وصل می‌شود. شناسه‌ها باید یکتا باشند. کپی component بدون اصلاح idها می‌تواند چند کنترل را به پنل اشتباه وصل کند.

aria-selected باید با پنل نمایش‌داده‌شده هماهنگ باشد. فقط تغییر رنگ Tab فعال، حالت برنامه‌ای را به‌روز نمی‌کند. پنل‌های غیر فعال معمولاً باید از نمایش و مسیر Focus خارج شوند. اگر فقط opacity را صفر کنید، عناصر پنهان ممکن است همچنان در Keyboard قابل انتخاب باشند. مدیریت hidden با state انتخاب در یک نقطه مشخص نگهداری شود.

Tab فعال باید بدون اتکا به رنگ قابل تشخیص باشد. underline، مرز یا شکل مناسب می‌تواند کمک کند. Focus با انتخاب متفاوت است؛ کاربر ممکن است Focus را روی Tab دیگری ببرد ولی هنوز همان پنل را ببیند. در activation دستی، این دو وضعیت باید جدا طراحی شوند تا حرکت میان عنوان‌ها به اشتباه تغییر محتوای ناخواسته ایجاد نکند.

Automatic و Manual Activation را آگاهانه انتخاب کنید

در activation خودکار، رفتن Focus به Tab پنل را هم فعال می‌کند. این روش زمانی مناسب‌تر است که محتوا بدون تأخیر محسوس آماده باشد. اگر هر حرکت درخواست شبکه ایجاد کند، کاربر برای مرور چند نام مجبور به انتظار می‌شود. APG برای چنین تفاوتی روش دستی را توضیح می‌دهد. انتخاب باید با رفتار بارگیری محتوا هماهنگ باشد.

در روش دستی، کلیدهای جهت Focus را حرکت می‌دهند و Enter یا Space پنل را فعال می‌کند. Home و End می‌توانند ابتدا و انتهای گروه را هدف بگیرند. در Tablist عمودی، بالا و پایین معنا دارند. برای RTL افقی، جهت حرکت را با چیدمان دیداری هماهنگ کنید و قرارداد را در component روشن بنویسید؛ انتقال بی‌فکر رفتار LTR ممکن است نامتعارف شود.

کلید Tab باید راه خروج از گروه و رسیدن به محتوای پنل را فراهم کند. قرار دادن همه عنوان‌ها در مسیر Tab می‌تواند یک گروه را به توقف‌های زیاد تبدیل کند. Roving tabindex مسیر داخل گروه را مدیریت می‌کند. اگر داخل پنل هیچ عنصر قابل Focus وجود ندارد، خود پنل می‌تواند نقطه قابل دسترس مناسبی برای ورود Keyboard باشد.

Accordion با دکمه و عنوان روشن کار می‌کند

الگوی Accordion در APG دکمه داخل heading و state expanded را معرفی می‌کند. نام بخش باید از روی header قابل فهم باشد. آیکون plus یا chevron فقط نشانه کمکی است. قرار دادن لینک عملی داخل همان دکمه می‌تواند تعامل تو در تو نامعتبر بسازد؛ عمل جدا باید ساختار مناسب خودش را داشته باشد.

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

عنصر details با summary برای Disclosure بومی قابل استفاده است و رفتار پایه بدون JavaScript دارد. آن را با الگوی کامل Tabs یکی نگیرید. برای چند پرسش مستقل می‌تواند راه ساده‌ای باشد. اگر نیاز پیچیده‌ای مثل کنترل از خارج یا قواعد خاص گروه دارید، قرارداد اضافی باید جدا تعریف شود.

نمونه کامل Tabs دستی و Disclosure بومی

کد زیر را در index.html ذخیره و باز کنید. سه پنل متنی از ابتدا در HTML وجود دارند و با خاموش بودن script هم قابل خواندن‌اند. بعد از آماده شدن JavaScript، کنترل‌ها به Tabs دستی تبدیل می‌شوند. بخش پرسش‌ها از details استفاده می‌کند و چند مورد می‌توانند باز بمانند. dependency، فایل CSS جدا و تصویر لازم ندارد.

<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Tabs دستی و Disclosure</title>
  <style>
    body { margin:0; color:#17212b; background:#f4f6f9; font:1rem/1.9 system-ui,sans-serif; }
    main { max-inline-size:45rem; margin-inline:auto; padding:2rem 1rem; }
    .tab-controls { display:flex; flex-wrap:wrap; gap:.5rem; }
    button { font:inherit; padding:.4rem .8rem; background:#fff; color:#17212b; border:1px solid #64748b; }
    [aria-selected="true"] { border-block-end:4px solid #164ca4; font-weight:700; }
    section,details { background:#fff; border:1px solid #64748b; padding:1rem; margin-block:1rem; }
    summary { cursor:pointer; font-weight:700; }
    :focus-visible { outline:3px solid #164ca4; outline-offset:3px; }
    [hidden] { display:none; }
    @media print { [role="tabpanel"][hidden] { display:block; } .tab-controls { display:none; } }
  </style>
</head>
<body>
  <main>
    <h1>اطلاعات دوره نمونه</h1>
    <div id="tabs" class="tab-controls" hidden>
      <button id="tab-outline" type="button">سرفصل</button>
      <button id="tab-format" type="button">روش آموزش</button>
      <button id="tab-requirements" type="button">پیش‌نیاز</button>
    </div>
    <section id="panel-outline"><h2>سرفصل</h2><p>این بخش متن آموزشی ثابت درباره سرفصل نمونه دارد.</p></section>
    <section id="panel-format"><h2>روش آموزش</h2><p>این بخش توضیح محلی درباره تمرین و مطالعه نمونه دارد.</p></section>
    <section id="panel-requirements"><h2>پیش‌نیاز</h2><p>این دوره واقعی نیست و پیش‌نیاز ثبت‌نام واقعی تعریف نمی‌کند.</p></section>
    <h2>پرسش‌های مستقل</h2>
    <details><summary>آیا این مثال داده‌ای ثبت می‌کند؟</summary><p>خیر؛ همه محتوای آن محلی و ثابت است.</p></details>
    <details><summary>آیا چند توضیح می‌تواند باز باشد؟</summary><p>بله؛ هر Disclosure در این نمونه مستقل است.</p></details>
  </main>
  <script>
    const group = document.getElementById('tabs');
    const tabs = [...group.querySelectorAll('button')];
    const panels = ['panel-outline','panel-format','panel-requirements'].map(id => document.getElementById(id));
    group.setAttribute('role','tablist');
    group.setAttribute('aria-label','اطلاعات دوره نمونه');
    function select(index) {
      tabs.forEach((tab,itemIndex) => {
        const active = index === itemIndex;
        tab.setAttribute('aria-selected',String(active));
        tab.tabIndex = active ? 0 : -1;
        panels[itemIndex].hidden = !active;
      });
    }
    tabs.forEach((tab,index) => {
      tab.setAttribute('role','tab');
      tab.setAttribute('aria-controls',panels[index].id);
      panels[index].setAttribute('role','tabpanel');
      panels[index].setAttribute('aria-labelledby',tab.id);
      panels[index].tabIndex = 0;
      tab.addEventListener('click',() => select(index));
      tab.addEventListener('keydown',event => {
        let next = index;
        if (event.key === 'ArrowLeft') next = (index + 1) % tabs.length;
        else if (event.key === 'ArrowRight') next = (index - 1 + tabs.length) % tabs.length;
        else if (event.key === 'Home') next = 0;
        else if (event.key === 'End') next = tabs.length - 1;
        else return;
        event.preventDefault();
        tabs.forEach((item,itemIndex) => { item.tabIndex = itemIndex === next ? 0 : -1; });
        tabs[next].focus();
      });
    });
    select(0);
    group.hidden = false;
  </script>
</body>
</html>

در نمونه، حرکت جهت‌ها فقط Focus را عوض می‌کند و انتخاب با Enter، Space یا کلیک انجام می‌شود. در RTL، چپ به عنوان بعدی و راست به قبلی می‌رود. پنل منتخب tabindex دارد تا متن ثابت آن در مسیر Keyboard قابل دسترسی باشد. state و hidden همزمان تغییر می‌کنند؛ انتخاب دیداری و محتوای واقعی دو منبع جدا ندارند.

محتوای همه پنل‌ها محلی است، بنابراین مثال ادعای حل دریافت async ندارد. در محصولی که پنل تازه بارگیری می‌شود، loading، error و retry لازم‌اند. مقاله Loading States برای این stateها ادامه مرتبط است. تعویض Tab نباید داده قبلی یا پاسخ دیررس را بدون بررسی روی پنل دیگری بنویسد.

Responsive و لینک مستقیم را از ابتدا در نظر بگیرید

نام Tabها در عرض کوچک می‌تواند چند سطر شود یا گروه scroll محدود داشته باشد. ترتیب و نشانه فعال باید باقی بمانند. تبدیل خودکار Tab به Accordion قرارداد جدیدی ایجاد می‌کند؛ اگر این تبدیل لازم است، state، Focus و معنای کنترل را هم تغییر دهید. تغییر صرفاً CSS بدون اصلاح roleها می‌تواند ظاهر و رفتار را ناسازگار کند.

لینک مستقیم به محتوای پنهان باید پنل مربوط را باز کند یا مسیر روشن دیگری بدهد. اگر hash به بخشی در پنل غیر فعال می‌رود، کاربر نباید صفحه خالی ببیند. حفظ انتخاب در URL می‌تواند به بازگشت کمک کند، ولی باید با هدف محصول و داده حساس هماهنگ باشد. هر Tab نیازی به URL مستقل ندارد؛ قرارداد تصمیم را ثبت کنید.

در چاپ یا خواندن بدون script، محتوای مهم نباید حذف شود. می‌توانید برای چاپ همه بخش‌ها را نمایش دهید. جست‌وجوی متن مرورگر هم ممکن است با محتوای پنهان رفتار متفاوتی داشته باشد؛ ساختار را با نیاز یافتن محتوا هماهنگ کنید. اگر کاربران دائماً دنبال متن در چند پنل‌اند، تقسیم محتوا شاید نیاز به بازنگری داشته باشد.

متن و قواعد component را همراه هم تحویل دهید

نام‌ها باید تفاوت بخش‌ها را روشن کنند. «اطلاعات» و «جزئیات» کنار هم ممکن است مرز موضوعی نداشته باشند. UX Writing برای نام کوتاه و دقیق کمک می‌کند. متن توضیحی ضروری را به عنوان Accordion نامعلوم منتقل نکنید؛ تصمیم درباره پنهان شدن باید همراه ویرایش محتوا حفظ شود.

در مستند component، روش activation، کلیدها، state اولیه، تعداد بخش باز و fallback را ثبت کنید. دسترسی‌پذیری و Visual Hierarchy ادامه‌های مرتبط‌اند. از کاربر بخواهید اطلاعات مشخصی را پیدا و مقایسه کند؛ اگر مرتب پنل‌ها را اشتباه باز می‌کند، گروه‌بندی و نام‌ها را قبل از افزودن animation اصلاح کنید.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!