26.01.2026

React’te Klavye Erişilebilirliği: “Roving Tabindex” ile Menü ve Liste Gezintisi

Klavye ile sorunsuz gezilen menü/liste bileşenleri için roving tabindex desenini React’te uygulayalım.

Neden “Roving Tabindex”?

Bir menü, sekme listesi veya seçenek listesi düşünün: Kullanıcı Tab ile bileşene girer, sonra ok tuşları ile öğeler arasında dolaşır. Her öğeyi tabIndex=0 yapmak odak sırasını uzatır ve kullanıcıyı yorar.

Roving tabindex yaklaşımında:

  • Sadece aktif öğe tabIndex=0 olur.
  • Diğerleri tabIndex=-1 olur.
  • Ok tuşlarıyla aktif indeks değişir, odak yeni öğeye taşınır.

Bu desen hem erişilebilirlik hem de UX açısından “doğru his” verir.


Hedef: Basit bir “Action List” bileşeni

Aşağıda, klavyeyle gezilebilen bir liste yapacağız:

  • Tab: listeye girer (aktif öğeye odaklanır)
  • ArrowDown/ArrowUp: öğeler arasında dolaşır
  • Home/End: başa/sona gider
  • Enter/Space: seçimi tetikler

Not: Örnek, erişilebilir bir temel sunar. “Seçili” durum (aria-selected) gibi ihtiyaçlara göre genişletebilirsiniz.


Uygulama (React)

import { useEffect, useMemo, useRef, useState } from "react";

function clampIndex(next, len) {
  if (len === 0) return 0;
  return (next + len) % len; // wrap-around
}

export function ActionList({ items, onActivate }) {
  const [activeIndex, setActiveIndex] = useState(0);
  const refs = useRef([]);

  // Items değişince index taşmasın
  useEffect(() => {
    setActiveIndex((i) => Math.min(i, Math.max(items.length - 1, 0)));
  }, [items.length]);

  const focusItem = (i) => {
    const el = refs.current[i];
    if (el) el.focus();
  };

  const onKeyDown = (e) => {
    const len = items.length;
    if (len === 0) return;

    const key = e.key;
    let next = activeIndex;

    if (key === "ArrowDown") next = clampIndex(activeIndex + 1, len);
    else if (key === "ArrowUp") next = clampIndex(activeIndex - 1, len);
    else if (key === "Home") next = 0;
    else if (key === "End") next = len - 1;
    else if (key === "Enter" || key === " ") {
      e.preventDefault();
      onActivate?.(items[activeIndex], activeIndex);
      return;
    } else {
      return; // ilgilenmiyoruz
    }

    e.preventDefault();
    setActiveIndex(next);
    // State güncellenmeden önce odak taşımak için microtask
    queueMicrotask(() => focusItem(next));
  };

  // A11y: Liste konteyneri değil, öğeler odak alıyor.
  // role=listbox yerine menü istiyorsanız role=menu + role=menuitem tercih edin.
  return (
    <div role="listbox" aria-label="Actions" onKeyDown={onKeyDown}>
      {items.map((item, i) => (
        <button
          key={item.id}
          ref={(el) => (refs.current[i] = el)}
          type="button"
          role="option"
          tabIndex={i === activeIndex ? 0 : -1}
          aria-selected={i === activeIndex}
          onClick={() => onActivate?.(item, i)}
          onFocus={() => setActiveIndex(i)}
          style={{
            display: "block",
            width: "100%",
            textAlign: "left",
            padding: 10,
            border: "1px solid #ddd",
            background: i === activeIndex ? "#f5f5f5" : "white",
          }}
        >
          {item.label}
        </button>
      ))}
    </div>
  );
}

// kullanım
// <ActionList items={[{id:'1', label:'Arşivle'}]} onActivate={(item)=>...} />


İnce noktalar (sık yapılan hatalar)

  1. Odak mı, aktif mi?

    • Ok tuşlarıyla “aktif indeks” değişince odak da değişmeli. Aksi halde ekran okuyucu/klavye deneyimi kopuk olur.
  2. onFocus ile senkronizasyon

    • Mouse ile tıklayınca veya Shift+Tab ile geri gelince activeIndex güncellenmeli.
  3. Wrap-around davranışı

    • Yukarıda mod alma ile başa/sona sarma yaptık. İstemiyorsanız clamp ile 0-len-1 arasında sabitleyin.
  4. Doğru ARIA rolü

    • Bu örnek “seçenek listesi” gibi davrandığı için listbox/option kullandı.
    • Gerçek bir uygulama menüsü için menu/menuitem daha uygun olabilir.

Kapanış

Roving tabindex, “küçük bir detay” gibi görünür ama klavye kullanıcıları için uygulamayı dramatik şekilde iyileştirir. Özellikle komut paleti, dropdown, sekme barı ve yan menü gibi alanlarda hızlıca değer üretir.